diff --git a/src/constructor.jl b/src/constructor.jl index 88be658..393a8ab 100644 --- a/src/constructor.jl +++ b/src/constructor.jl @@ -1,19 +1,21 @@ +# Error for constructors struct ConstructorError <: Exception - context::Union{String, Nothing} - context_mark::Union{Mark, Nothing} - problem::Union{String, Nothing} - problem_mark::Union{Mark, Nothing} - note::Union{String, Nothing} - - function ConstructorError(context=nothing, context_mark=nothing, - problem=nothing, problem_mark=nothing, - note=nothing) - new(context, context_mark, problem, problem_mark, note) - end - + context :: Union{String, Nothing} + context_mark :: Union{Mark, Nothing} + problem :: Union{String, Nothing} + problem_mark :: Union{Mark, Nothing} + note :: Union{String, Nothing} end +# `context` at `context_mark`: `problem` at `problem_mark` +ConstructorError(context, context_mark, problem, problem_mark) = + ConstructorError(context, context_mark, problem, problem_mark, nothing) + +# `problem` at `problem_mark` +ConstructorError(problem, problem_mark) = + ConstructorError(nothing, nothing, problem, problem_mark) + function show(io::IO, error::ConstructorError) if error.context !== nothing print(io, error.context, " at ", error.context_mark, ": ") @@ -21,33 +23,51 @@ function show(io::IO, error::ConstructorError) print(io, error.problem, " at ", error.problem_mark) end +# Constructor mutable struct Constructor - constructed_objects::Dict{Node, Any} - recursive_objects::Set{Node} - yaml_constructors::Dict{Union{String, Nothing}, Function} - yaml_multi_constructors::Dict{Union{String, Nothing}, Function} - - function Constructor(single_constructors = Dict(), multi_constructors = Dict()) - new(Dict{Node, Any}(), Set{Node}(), - convert(Dict{Union{String, Nothing},Function}, single_constructors), - convert(Dict{Union{String, Nothing},Function}, multi_constructors)) - end + constructed_objects :: Dict{Node, Any} + recursive_objects :: Set{Node} + yaml_constructors :: Dict{Union{String, Nothing}, Function} + yaml_multi_constructors :: Dict{Union{String, Nothing}, Function} + + Constructor( + # Can we add type annotations? + single_constructors = Dict{String, Function}(), + multi_constructors = Dict{String, Function}(), + ) = new( + Dict{Node, Any}(), + Set{Node}(), + convert(Dict{Union{String, Nothing}, Function}, single_constructors), + convert(Dict{Union{String, Nothing}, Function}, multi_constructors), + ) end +Constructor(::Nothing) = Constructor(Dict{String, Function}()) +# add a constructor function of the specific tag function add_constructor!(func::Function, constructor::Constructor, tag::Union{String, Nothing}) constructor.yaml_constructors[tag] = func constructor end +# add a multi constructor function of the specific tag function add_multi_constructor!(func::Function, constructor::Constructor, tag::Union{String, Nothing}) constructor.yaml_multi_constructors[tag] = func constructor end -Constructor(::Nothing) = Constructor(Dict{String,Function}()) -SafeConstructor(constructors::Dict = Dict(), multi_constructors::Dict = Dict()) = Constructor(merge(copy(default_yaml_constructors), constructors), multi_constructors) +# Paalon: I don't know what is safe. +SafeConstructor( + # Can we add more specific type annotations? + constructors :: Dict = Dict(), + multi_constructors :: Dict = Dict(), +) = Constructor( + merge(copy(yaml_jl_0_4_10_schema_constructors), constructors), + multi_constructors, +) + +# construct_document function construct_document(constructor::Constructor, node::Node) data = construct_object(constructor, node) @@ -58,16 +78,12 @@ end construct_document(::Constructor, ::MissingDocument) = missing_document +# construct_object + function construct_object(constructor::Constructor, node::Node) - if haskey(constructor.constructed_objects, node) - return constructor.constructed_objects[node] - end + haskey(constructor.constructed_objects, node) && return constructor.constructed_objects[node] - if in(node, constructor.recursive_objects) - throw(ConstructorError(nothing, nothing, - "found unconstructable recursive node", - node.start_mark)) - end + node in constructor.recursive_objects && throw(ConstructorError("found unconstructable recursive node", node.start_mark)) push!(constructor.recursive_objects, node) node_constructor = nothing @@ -113,32 +129,35 @@ function construct_object(constructor::Constructor, node::Node) data end +# construct_scalar function construct_scalar(constructor::Constructor, node::Node) - if !(node isa ScalarNode) - throw(ConstructorError(nothing, nothing, - "expected a scalar node, but found $(typeof(node))", - node.start_mark)) - end + node isa ScalarNode || throw(ConstructorError("expected a scalar node, but found $(typeof(node))", node.start_mark)) node.value end +# construct_sequence function construct_sequence(constructor::Constructor, node::Node) - if !(node isa SequenceNode) - throw(ConstructorError(nothing, nothing, - "expected a sequence node, but found $(typeof(node))", - node.start_mark)) - end - + node isa SequenceNode || throw(ConstructorError("expected a sequence node, but found $(typeof(node))", node.start_mark)) [construct_object(constructor, child) for child in node.value] end +# flatten_mapping + +# TODO: +# This function processes the following 2 tags: +# - "tag:yaml.org,2002:merge" +# - "tag:yaml.org,2002:value" +# So, we need to investigate. function flatten_mapping(node::MappingNode) + # TODO: + # The variable name `merge` is exported from Julia `Base` + # thus it should be renamed for disambiguation. merge = [] index = 1 - while index <= length(node.value) + while index ≤ length(node.value) key_node, value_node = node.value[index] if key_node.tag == "tag:yaml.org,2002:merge" node.value = node.value[setdiff(axes(node.value, 1), index)] @@ -148,12 +167,10 @@ function flatten_mapping(node::MappingNode) elseif value_node isa SequenceNode submerge = [] for subnode in value_node.value - if !(subnode isa MappingNode) - throw(ConstructorError("while constructing a mapping", - node.start_mark, - "expected a mapping node, but found $(typeof(subnode))", - subnode.start_mark)) - end + subnode isa MappingNode || throw(ConstructorError( + "while constructing a mapping", node.start_mark, + "expected a mapping node, but found $(typeof(subnode))", subnode.start_mark, + )) flatten_mapping(subnode) push!(submerge, subnode.value) for value in reverse(submerge) @@ -172,10 +189,12 @@ function flatten_mapping(node::MappingNode) if !isempty(merge) node.value = vcat(merge, node.value) end + nothing end +# construct_mapping -function construct_mapping(dicttype::Union{Type,Function}, constructor::Constructor, node::MappingNode) +function construct_mapping(dicttype::Union{Type, Function}, constructor::Constructor, node::MappingNode) flatten_mapping(node) mapping = dicttype() for (key_node, value_node) in node.value @@ -185,17 +204,13 @@ function construct_mapping(dicttype::Union{Type,Function}, constructor::Construc try key = keytype(mapping)(key) # try to cast catch - throw(ConstructorError(nothing, nothing, - "Cannot cast $key to the key type of $dicttype", - node.start_mark)) + throw(ConstructorError("Cannot cast $key to the key type of $dicttype", node.start_mark)) end end try mapping[key] = value catch - throw(ConstructorError(nothing, nothing, - "Cannot store $key=>$value in $dicttype", - node.start_mark)) + throw(ConstructorError("Cannot store $key=>$value in $dicttype", node.start_mark)) end end mapping @@ -203,225 +218,24 @@ end construct_mapping(constructor::Constructor, node::Node) = construct_mapping(Dict{Any,Any}, constructor, node) +# custom_mapping # create a construct_mapping instance for a specific dicttype + custom_mapping(dicttype::Type{D}) where D <: AbstractDict = (constructor::Constructor, node::Node) -> construct_mapping(dicttype, constructor, node) + function custom_mapping(dicttype::Function) - dicttype_test = try dicttype() catch + dicttype_test = try + dicttype() + catch throw(ArgumentError("The dicttype Function cannot be called without arguments")) end - if !(dicttype_test isa AbstractDict) - throw(ArgumentError("The dicttype Function does not return an AbstractDict")) - end - return (constructor::Constructor, node::Node) -> construct_mapping(dicttype, constructor, node) -end - - -function construct_yaml_null(constructor::Constructor, node::Node) - construct_scalar(constructor, node) - nothing -end - - -const bool_values = Dict( - "yes" => true, - "no" => false, - "true" => true, - "false" => false, - "on" => true, - "off" => false ) - - -function construct_yaml_bool(constructor::Constructor, node::Node) - value = construct_scalar(constructor, node) - bool_values[lowercase(value)] -end - - -function construct_yaml_int(constructor::Constructor, node::Node) - value = string(construct_scalar(constructor, node)) - value = lowercase(replace(value, "_" => "")) - - if in(':', value) - # TODO - #throw(ConstructorError(nothing, nothing, - #"sexagesimal integers not yet implemented", node.start_mark)) - @warn "sexagesimal integers not yet implemented. Returning String." - return value - end - - if length(value) > 2 && value[1] == '0' && (value[2] == 'x' || value[2] == 'X') - return parse(Int, value[3:end], base = 16) - elseif length(value) > 1 && value[1] == '0' - return parse(Int, value, base = 8) - else - return parse(Int, value, base = 10) - end -end - - -function construct_yaml_float(constructor::Constructor, node::Node) - value = string(construct_scalar(constructor, node)) - value = lowercase(replace(value, "_" => "")) - - if in(':', value) - # TODO - # throw(ConstructorError(nothing, nothing, - # "sexagesimal floats not yet implemented", node.start_mark)) - @warn "sexagesimal floats not yet implemented. Returning String." - return value - end - - if value == ".nan" - return NaN - end - - m = match(r"^([+\-]?)\.inf$", value) - if m !== nothing - if m.captures[1] == "-" - return -Inf - else - return Inf - end - end - - return parse(Float64, value) -end - - -const timestamp_pat = - r"^(\d{4})- (?# year) - (\d\d?)- (?# month) - (\d\d?) (?# day) - (?: - (?:[Tt]|[ \t]+) - (\d\d?): (?# hour) - (\d\d): (?# minute) - (\d\d) (?# second) - (?:\.(\d*))? (?# fraction) - (?: - [ \t]*(Z|(?:[+\-])(\d\d?) - (?: - :(\d\d) - )?) - )? - )?$"x - - -function construct_yaml_timestamp(constructor::Constructor, node::Node) - value = construct_scalar(constructor, node) - mat = match(timestamp_pat, value) - if mat === nothing - throw(ConstructorError(nothing, nothing, - "could not make sense of timestamp format", node.start_mark)) - end - - yr = parse(Int, mat.captures[1]) - mn = parse(Int, mat.captures[2]) - dy = parse(Int, mat.captures[3]) - - if mat.captures[4] === nothing - return Date(yr, mn, dy) - end - - h = parse(Int, mat.captures[4]) - m = parse(Int, mat.captures[5]) - s = parse(Int, mat.captures[6]) - - if mat.captures[7] === nothing - return DateTime(yr, mn, dy, h, m, s) - end - - ms = 0 - if mat.captures[7] !== nothing - ms = mat.captures[7] - if length(ms) > 3 - ms = ms[1:3] - end - ms = parse(Int, string(ms, repeat("0", 3 - length(ms)))) - end - - delta_hr = 0 - delta_mn = 0 - - if mat.captures[9] !== nothing - delta_hr = parse(Int, mat.captures[9]) - end - - if mat.captures[10] !== nothing - delta_mn = parse(Int, mat.captures[10]) - end - - # TODO: Also, I'm not sure if there is a way to numerically set the timezone - # in Calendar. - - return DateTime(yr, mn, dy, h, m, s, ms) -end - - -function construct_yaml_omap(constructor::Constructor, node::Node) - throw(ConstructorError(nothing, nothing, - "omap type not yet implemented", node.start_mark)) -end - - -function construct_yaml_pairs(constructor::Constructor, node::Node) - throw(ConstructorError(nothing, nothing, - "pairs type not yet implemented", node.start_mark)) -end - - -function construct_yaml_set(constructor::Constructor, node::Node) - throw(ConstructorError(nothing, nothing, - "set type not yet implemented", node.start_mark)) -end - - -function construct_yaml_str(constructor::Constructor, node::Node) - string(construct_scalar(constructor, node)) -end - - -function construct_yaml_seq(constructor::Constructor, node::Node) - construct_sequence(constructor, node) -end - - -function construct_yaml_map(constructor::Constructor, node::Node) - construct_mapping(constructor, node) -end - - -function construct_yaml_object(constructor::Constructor, node::Node) - throw(ConstructorError(nothing, nothing, - "object type not yet implemented", node.start_mark)) -end - - -function construct_undefined(constructor::Constructor, node::Node) - throw(ConstructorError(nothing, nothing, - "could not determine a constructor for the tag '$(node.tag)'", - node.start_mark)) -end - - -function construct_yaml_binary(constructor::Constructor, node::Node) - value = replace(string(construct_scalar(constructor, node)), "\n" => "") - base64decode(value) + dicttype_test isa AbstractDict || throw(ArgumentError("The dicttype Function does not return an AbstractDict")) + (constructor::Constructor, node::Node) -> construct_mapping(dicttype, constructor, node) end -const default_yaml_constructors = Dict{Union{String, Nothing}, Function}( - "tag:yaml.org,2002:null" => construct_yaml_null, - "tag:yaml.org,2002:bool" => construct_yaml_bool, - "tag:yaml.org,2002:int" => construct_yaml_int, - "tag:yaml.org,2002:float" => construct_yaml_float, - "tag:yaml.org,2002:binary" => construct_yaml_binary, - "tag:yaml.org,2002:timestamp" => construct_yaml_timestamp, - "tag:yaml.org,2002:omap" => construct_yaml_omap, - "tag:yaml.org,2002:pairs" => construct_yaml_pairs, - "tag:yaml.org,2002:set" => construct_yaml_set, - "tag:yaml.org,2002:str" => construct_yaml_str, - "tag:yaml.org,2002:seq" => construct_yaml_seq, - "tag:yaml.org,2002:map" => construct_yaml_map, - nothing => construct_undefined, - ) +# Definition of constructors for each schema. +include("constructor_failsafe.jl") +include("constructor_json.jl") +include("constructor_core.jl") +include("constructor_yaml_jl_0_4_10.jl") diff --git a/src/constructor_core.jl b/src/constructor_core.jl new file mode 100644 index 0000000..6b89170 --- /dev/null +++ b/src/constructor_core.jl @@ -0,0 +1,103 @@ +# Constructors for the Core schema. + +# Parsing utils + +struct CoreSchemaParseError <: Exception end + +function tryparse_core_schema_null(str::String)::Union{Nothing, CoreSchemaParseError} + # nothing + str == "null" || str == "Null" || str == "NULL" || str == "~" ? nothing : + # error + CoreSchemaParseError() +end + +function tryparse_core_schema_bool(str::String)::Union{Bool, CoreSchemaParseError} + # true + str == "true" || str == "True" || str == "TRUE" ? true : + # false + str == "false" || str == "False" || str == "FALSE" ? false : + # error + CoreSchemaParseError() +end + +function tryparse_core_schema_int(str::String)::Union{Int, CoreSchemaParseError} + n = + # hexadecimal + length(str) > 2 && str[1] == '0' && str[2] == 'x' ? tryparse(Int, str[3:end], base=16) : + # octal + length(str) > 2 && str[1] == '0' && str[2] == 'o' ? tryparse(Int, str[3:end], base=8) : + # decimal + tryparse(Int, str, base=10) + # int + n !== nothing ? n : + # error + CoreSchemaParseError() +end + +function tryparse_core_schema_float(str::String)::Union{Float64, CoreSchemaParseError} + # not a number + (str == ".nan" || str == ".NaN" || str == ".NAN") && return NaN + # infinity + m = match(r"^([-+]?)(\.inf|\.Inf|\.INF)$", str) + m !== nothing && return m.captures[1] == "-" ? -Inf : Inf + # fixed or exponential + x = tryparse(Float64, str) + # float + x !== nothing && isfinite(x) ? x : + # error + CoreSchemaParseError() +end + +# Construct functions + +construct_undefined_core_schema(constructor::Constructor, node::Node) = + throw(ConstructorError("could not determine a constructor for the tag '$(node.tag)' in the Core schema", node.start_mark)) + +const construct_core_schema_str = construct_failsafe_schema_str + +const construct_core_schema_seq = construct_failsafe_schema_seq + +const construct_core_schema_map = construct_failsafe_schema_map + +function construct_core_schema_null(constructor::Constructor, node::Node)::Nothing + str = construct_scalar(constructor, node) + n = tryparse_core_schema_null(str) + n isa CoreSchemaParseError && + throw(ConstructorError("could not construct a null '$str' in the Core schema", node.start_mark)) + n +end + +function construct_core_schema_bool(constructor::Constructor, node::Node)::Bool + str = construct_scalar(constructor, node) + b = tryparse_core_schema_bool(str) + b isa CoreSchemaParseError && + throw(ConstructorError("could not construct a bool '$str' in the Core schema", node.start_mark)) + b +end + +function construct_core_schema_int(constructor::Constructor, node::Node)::Int + str = construct_scalar(constructor, node) + n = tryparse_core_schema_int(str) + n isa CoreSchemaParseError && + throw(ConstructorError("could not construct an int '$str' in the Core schema", node.start_mark)) + n +end + +function construct_core_schema_float(construct::Constructor, node::Node)::Float64 + str = construct_scalar(constructor, node) + x = tryparse_core_schema_float(str) + x isa CoreSchemaParseError && + throw(ConstructorError("could not construct a float '$str' in the Core schema", node.start_mark)) + x +end + +const core_schema_constructors = Dict{Union{String, Nothing}, Function}( + nothing => construct_undefined_core_schema, + "tag:yaml.org,2002:str" => construct_core_schema_str, + "tag:yaml.org,2002:seq" => construct_core_schema_seq, + "tag:yaml.org,2002:map" => construct_core_schema_map, + "tag:yaml.org,2002:null" => construct_core_schema_null, + "tag:yaml.org,2002:bool" => construct_core_schema_bool, + "tag:yaml.org,2002:int" => construct_core_schema_int, + "tag:yaml.org,2002:float" => construct_core_schema_float, +) diff --git a/src/constructor_failsafe.jl b/src/constructor_failsafe.jl new file mode 100644 index 0000000..17044a0 --- /dev/null +++ b/src/constructor_failsafe.jl @@ -0,0 +1,26 @@ +# Constructors for the failsafe schema. + +# Parsing utils + +struct FailsafeSchemaParseError <: Exception end + +# Construct functions + +construct_undefined_failsafe_schema(constructor::Constructor, node::Node) = + throw(ConstructorError("could not determine a constructor for the tag '$(node.tag)' in the failsafe schema", node.start_mark)) + +construct_failsafe_schema_str(constructor::Constructor, node::Node) = + construct_scalar(constructor, node) + +construct_failsafe_schema_seq(constructor::Constructor, node::Node) = + construct_sequence(constructor, node) + +construct_failsafe_schema_map(constructor::Constructor, node::Node) = + construct_mapping(constructor, node) + +const failsafe_schema_constructors = Dict{Union{String, Nothing}, Function}( + nothing => construct_undefined_failsafe_schema, + "tag:yaml.org,2002:str" => construct_failsafe_schema_str, + "tag:yaml.org,2002:seq" => construct_failsafe_schema_seq, + "tag:yaml.org,2002:map" => construct_failsafe_schema_map, +) diff --git a/src/constructor_json.jl b/src/constructor_json.jl new file mode 100644 index 0000000..9f829a8 --- /dev/null +++ b/src/constructor_json.jl @@ -0,0 +1,109 @@ +# Constructors for the JSON schema. + +# Parsing utils + +struct JSONSchemaParseError <: Exception end + +function tryparse_json_schema_null(str::String)::Union{Nothing, JSONSchemaParseError} + str == "null" ? nothing : + JSONSchemaParseError() +end + +function tryparse_json_schema_bool(str::String)::Union{Bool, JSONSchemaParseError} + str == "true" ? true : + str == "false" ? false : + JSONSchemaParseError() +end + +function tryparse_json_schema_int(str::String)::Union{Int, JSONSchemaParseError} + len = length(str) + if len ≥ 2 + if str[1] == '+' + # plus sign + return JSONSchemaParseError() + elseif str[1] == '0' + # leading zero + return JSONSchemaParseError() + elseif len > 2 && str[1] == '-' && str[2] == '0' + # minus sign + leading zero + return JSONSchemaParseError() + end + end + # decimal + n = tryparse(Int, str, base=10) + n === nothing && return JSONSchemaParseError() + n +end + +function tryparse_json_schema_float(str::String)::Union{Float64, JSONSchemaParseError} + len = length(str) + # plus sign + len ≥ 1 && str[1] == '+' && return JSONSchemaParseError() + # leading dot + len ≥ 1 && str[1] == '.' && return JSONSchemaParseError() + # minus sign + leading dot + len ≥ 2 && str[1] == '-' && str[2] == '.' && return JSONSchemaParseError() + # leading zero + len ≥ 2 && str[1] == '0' && str[2] ≠ '.' && return JSONSchemaParseError() + # minus sign + leading zero + len ≥ 3 && str[1] == '-' && str[2] == '0' && str[3] ≠ '.' && return JSONSchemaParseError() + # fixed or exponential + x = tryparse(Float64, str) + x === nothing && return JSONSchemaParseError() + !isfinite(x) && return JSONSchemaParseError() + x +end + +# Construct functions + +construct_undefined_json_schema(constructor::Constructor, node::Node) = + throw(ConstructorError("could not determine a constructor for the tag '$(node.tag)' in the JSON schema", node.start_mark)) + +const construct_json_schema_str = construct_failsafe_schema_str + +const construct_json_schema_seq = construct_failsafe_schema_seq + +const construct_json_schema_map = construct_failsafe_schema_map + +function construct_json_schema_null(constructor::Constructor, node::Node)::Nothing + str = construct_scalar(constructor, node) + n = tryparse_json_schema_null(str) + n isa JSONSchemaParseError && + throw(ConstructorError("could not construct a null '$str' in the JSON schema", node.start_mark)) + n +end + +function construct_json_schema_bool(constructor::Constructor, node::Node)::Bool + str = construct_scalar(constructor, node) + b = tryparse_json_schema_bool(str) + b isa JSONSchemaParseError && + throw(ConstructorError("could not construct a bool '$str' in the JSON schema", node.start_mark)) + b +end + +function construct_json_schema_int(constructor::Constructor, node::Node)::Int + str = construct_scalar(constructor, node) + n = tryparse_json_schema_int(str) + n isa JSONSchemaParseError && + throw(ConstructorError("could not construct an int '$str' in the JSON schema", node.start_mark)) + n +end + +function construct_json_schema_float(construct::Constructor, node::Node)::Float64 + str = construct_scalar(constructor, node) + x = tryparse_json_schema_float(str) + x isa JSONSchemaParseError && + throw(ConstructorError("could not construct a float '$str' in the JSON schema", node.start_mark)) + x +end + +const json_schema_constructors = Dict{Union{String, Nothing}, Function}( + nothing => construct_undefined_json_schema, + "tag:yaml.org,2002:str" => construct_json_schema_str, + "tag:yaml.org,2002:seq" => construct_json_schema_seq, + "tag:yaml.org,2002:map" => construct_json_schema_map, + "tag:yaml.org,2002:null" => construct_json_schema_null, + "tag:yaml.org,2002:bool" => construct_json_schema_bool, + "tag:yaml.org,2002:int" => construct_json_schema_int, + "tag:yaml.org,2002:float" => construct_json_schema_float, +) diff --git a/src/constructor_yaml_jl_0_4_10.jl b/src/constructor_yaml_jl_0_4_10.jl new file mode 100644 index 0000000..adcf3e8 --- /dev/null +++ b/src/constructor_yaml_jl_0_4_10.jl @@ -0,0 +1,194 @@ +# Constructors for the YAML.jl v0.4.10 schema. + +construct_undefined_yaml_jl_0_4_10_schema(constructor::Constructor, node::Node) = + throw(ConstructorError("could not determine a constructor for the tag '$(node.tag)' the YAML.jl v0.4.0 schema", node.start_mark)) + +construct_yaml_jl_0_4_10_schema_str(constructor::Constructor, node::Node) = + construct_scalar(constructor, node) + +construct_yaml_jl_0_4_10_schema_seq(constructor::Constructor, node::Node) = + construct_sequence(constructor, node) + +construct_yaml_jl_0_4_10_schema_map(constructor::Constructor, node::Node) = + construct_mapping(constructor, node) + +function construct_yaml_jl_0_4_10_schema_null(constructor::Constructor, node::Node) + _ = construct_scalar(constructor, node) + nothing +end + +# TODO: There is no resolver definition of +# - yes +# - no +# - on +# - off +# in resolver.jl. It's strange. Why do they exist here? +const yaml_jl_0_4_10_schema_bool_values = Dict( + "yes" => true, + "no" => false, + "true" => true, + "false" => false, + "on" => true, + "off" => false, +) + +function construct_yaml_jl_0_4_10_schema_bool(constructor::Constructor, node::Node) + str = construct_scalar(constructor, node) + yaml_jl_0_4_10_schema_bool_values[lowercase(str)] +end + +function construct_yaml_jl_0_4_10_schema_int(constructor::Constructor, node::Node) + str = construct_scalar(constructor, node) + str = lowercase(replace(str, "_" => "")) + + # sexagesimal integers + if in(':', str) + # TODO: + # throw(ConstructorError("sexagesimal integers not yet implemented", node.start_mark)) + @warn "sexagesimal integers not yet implemented. Returning String." + return str + end + + # hexadecimal + if length(str) > 2 && str[1] == '0' && (str[2] == 'x' || str[2] == 'X') + parse(Int, str[3:end], base=16) + # octal + elseif length(str) > 1 && str[1] == '0' + parse(Int, str, base=8) + # decimal + else + parse(Int, str, base=10) + end +end + +function construct_yaml_jl_0_4_10_schema_float(constructor::Constructor, node::Node) + str = construct_scalar(constructor, node) + str = lowercase(replace(str, "_" => "")) + + # sexagesimal float + if in(':', str) + # TODO: + # throw(ConstructorError("sexagesimal floats not yet implemented", node.start_mark)) + @warn "sexagesimal floats not yet implemented. Returning String." + return str + end + + # not a number + str == ".nan" && return NaN + + # infinity + m = match(r"^([+\-]?)\.inf$", str) + if m !== nothing + # negative infinity + if m.captures[1] == "-" + return -Inf + # positive infinity + else + return Inf + end + end + + # fixed or exponential + parse(Float64, str) +end + +const yaml_jl_0_4_10_schema_timestamp_regex = r"^ + (\d{4})- (?# year) + (\d\d?)- (?# month) + (\d\d?) (?# day) + (?: + (?:[Tt]|[ \t]+) + (\d\d?): (?# hour) + (\d\d): (?# minute) + (\d\d) (?# second) + (?:\.(\d*))? (?# fraction) + (?: + [ \t]*( + Z | + (?:[+\-])(\d\d?) + (?: + :(\d\d) + )? + ) + )? + )? +$"x + +function construct_yaml_jl_0_4_10_schema_timestamp(constructor::Constructor, node::Node) + str = construct_scalar(constructor, node) + mat = match(yaml_jl_0_4_10_schema_timestamp_regex, str) + mat === nothing && throw(ConstructorError("could not make sense of timestamp format", node.start_mark)) + + yr = parse(Int, mat.captures[1]) + mn = parse(Int, mat.captures[2]) + dy = parse(Int, mat.captures[3]) + mat.captures[4] === nothing && return Date(yr, mn, dy) + + h = parse(Int, mat.captures[4]) + m = parse(Int, mat.captures[5]) + s = parse(Int, mat.captures[6]) + mat.captures[7] === nothing && return DateTime(yr, mn, dy, h, m, s) + + ms = 0 + if mat.captures[7] !== nothing + ms = mat.captures[7] + if length(ms) > 3 + ms = ms[1:3] + end + ms = parse(Int, string(ms, repeat("0", 3 - length(ms)))) + end + + delta_hr = 0 + delta_mn = 0 + + if mat.captures[9] !== nothing + delta_hr = parse(Int, mat.captures[9]) + end + + if mat.captures[10] !== nothing + delta_mn = parse(Int, mat.captures[10]) + end + + # TODO: Also, I'm not sure if there is a way to numerically set the timezone + # in Calendar. + + DateTime(yr, mn, dy, h, m, s, ms) +end + +construct_yaml_jl_0_4_10_schema_omap(constructor::Constructor, node::Node) = + throw(ConstructorError("omap type not yet implemented", node.start_mark)) + +construct_yaml_jl_0_4_10_schema_pairs(constructor::Constructor, node::Node) = + throw(ConstructorError("pairs type not yet implemented", node.start_mark)) + +construct_yaml_jl_0_4_10_schema_set(constructor::Constructor, node::Node) = + throw(ConstructorError("set type not yet implemented", node.start_mark)) + +construct_yaml_jl_0_4_10_schema_object(constructor::Constructor, node::Node) = + throw(ConstructorError("object type not yet implemented", node.start_mark)) + +function construct_yaml_jl_0_4_10_schema_binary(constructor::Constructor, node::Node) + str = construct_scalar(constructor, node) + str = replace(str, "\n" => "") + base64decode(str) +end + +const yaml_jl_0_4_10_schema_constructors = Dict{Union{String, Nothing}, Function}( + nothing => construct_undefined_yaml_jl_0_4_10_schema, + "tag:yaml.org,2002:str" => construct_yaml_jl_0_4_10_schema_str, + "tag:yaml.org,2002:seq" => construct_yaml_jl_0_4_10_schema_seq, + "tag:yaml.org,2002:map" => construct_yaml_jl_0_4_10_schema_map, + "tag:yaml.org,2002:null" => construct_yaml_jl_0_4_10_schema_null, + "tag:yaml.org,2002:bool" => construct_yaml_jl_0_4_10_schema_bool, + "tag:yaml.org,2002:int" => construct_yaml_jl_0_4_10_schema_int, + "tag:yaml.org,2002:float" => construct_yaml_jl_0_4_10_schema_float, + "tag:yaml.org,2002:binary" => construct_yaml_jl_0_4_10_schema_binary, + "tag:yaml.org,2002:timestamp" => construct_yaml_jl_0_4_10_schema_timestamp, + "tag:yaml.org,2002:omap" => construct_yaml_jl_0_4_10_schema_omap, + "tag:yaml.org,2002:pairs" => construct_yaml_jl_0_4_10_schema_pairs, + "tag:yaml.org,2002:set" => construct_yaml_jl_0_4_10_schema_set, + # TODO: Investigate how these 3 tags are processed in the YAML.jl v0.4.10 schema. + # "tag:yaml.org,2002:merge" + # "tag:yaml.org,2002:value" + # "tag:yaml.org,2002:yaml" +) diff --git a/test/runtests.jl b/test/runtests.jl index 02bf675..abf06e4 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -288,20 +288,21 @@ end @testset "Custom Constructor" begin function MySafeConstructor() - yaml_constructors = copy(YAML.default_yaml_constructors) + yaml_constructors = copy(YAML.yaml_jl_0_4_10_schema_constructors) delete!(yaml_constructors, nothing) YAML.Constructor(yaml_constructors) end function MyReallySafeConstructor() - yaml_constructors = copy(YAML.default_yaml_constructors) + yaml_constructors = copy(YAML.yaml_jl_0_4_10_schema_constructors) delete!(yaml_constructors, nothing) ret = YAML.Constructor(yaml_constructors) YAML.add_multi_constructor!(ret, nothing) do constructor::YAML.Constructor, tag, node - throw(YAML.ConstructorError(nothing, nothing, - "could not determine a constructor for the tag '$(tag)'", - node.start_mark)) + throw(YAML.ConstructorError( + "could not determine a constructor for the tag '$(tag)' in the YAML.jl v0.4.10 schema", + node.start_mark, + )) end ret end @@ -470,4 +471,60 @@ end @test collect(YAML.load_all(input)) == expected end +@testset "failsafe schema" begin +end + +@testset "JSON schema" begin + @test YAML.tryparse_json_schema_null("A null") isa YAML.JSONSchemaParseError + @test YAML.tryparse_json_schema_null("null") === nothing + @test YAML.tryparse_json_schema_null("Null") isa YAML.JSONSchemaParseError + @test YAML.tryparse_json_schema_bool("true") == true + @test YAML.tryparse_json_schema_bool("True") isa YAML.JSONSchemaParseError + @test YAML.tryparse_json_schema_bool("false") == false + @test YAML.tryparse_json_schema_bool("FALSE") isa YAML.JSONSchemaParseError + @test YAML.tryparse_json_schema_int("0") == 0 + @test YAML.tryparse_json_schema_int("7") == 7 + @test YAML.tryparse_json_schema_int("58") == 58 + @test YAML.tryparse_json_schema_int("-19") == -19 + @test YAML.tryparse_json_schema_int("0o7") isa YAML.JSONSchemaParseError + @test YAML.tryparse_json_schema_int("0x3A") isa YAML.JSONSchemaParseError + @test YAML.tryparse_json_schema_float("0.") == 0.0 + @test YAML.tryparse_json_schema_float("-0.0") == -0.0 + @test YAML.tryparse_json_schema_float(".5") isa YAML.JSONSchemaParseError + @test YAML.tryparse_json_schema_float("12e03") == 12000 + @test YAML.tryparse_json_schema_float("+12e03") isa YAML.JSONSchemaParseError + @test YAML.tryparse_json_schema_float("-2E+05") == -200000 + @test YAML.tryparse_json_schema_float(".inf") isa YAML.JSONSchemaParseError + @test YAML.tryparse_json_schema_float("-.Inf") isa YAML.JSONSchemaParseError + @test YAML.tryparse_json_schema_float("+.INF") isa YAML.JSONSchemaParseError + @test YAML.tryparse_json_schema_float(".NAN") isa YAML.JSONSchemaParseError + @test YAML.tryparse_json_schema_float("+12.3") isa YAML.JSONSchemaParseError + +end + +@testset "Core schema" begin + @test YAML.tryparse_core_schema_null("A null") isa YAML.CoreSchemaParseError + @test YAML.tryparse_core_schema_null("null") === nothing + @test YAML.tryparse_core_schema_bool("true") == true + @test YAML.tryparse_core_schema_bool("True") == true + @test YAML.tryparse_core_schema_bool("false") == false + @test YAML.tryparse_core_schema_bool("FALSE") == false + @test YAML.tryparse_core_schema_int("0") == 0 + @test YAML.tryparse_core_schema_int("7") == 7 + @test YAML.tryparse_core_schema_int("58") == 58 + @test YAML.tryparse_core_schema_int("-19") == -19 + @test YAML.tryparse_core_schema_float("0.") == 0.0 + @test YAML.tryparse_core_schema_float("-0.0") == -0.0 + @test YAML.tryparse_core_schema_float(".5") == 0.5 + @test YAML.tryparse_core_schema_float("+12e03") == 12000 + @test YAML.tryparse_core_schema_float("-2E+05") == -200000 + @test YAML.tryparse_core_schema_float(".inf") == Inf + @test YAML.tryparse_core_schema_float("-.Inf") == -Inf + @test YAML.tryparse_core_schema_float("+.INF") == Inf + @test YAML.tryparse_core_schema_float(".NAN") |> isnan +end + +@testset "YAML.jl v0.4.10 schema" begin +end + end # module