From 065e75c1dcc1ef365bf54fa76e90f762e7117556 Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 11 May 2021 14:32:33 +1200 Subject: [PATCH 1/3] Move JSONSchema to a test dependency --- Project.toml | 7 ++- docs/Project.toml | 4 ++ docs/src/submodules/FileFormats/overview.md | 60 +++++++++++++++++++++ src/FileFormats/MOF/MOF.jl | 53 ++---------------- src/FileFormats/MOF/read.jl | 4 -- test/FileFormats/MOF/MOF.jl | 47 ++++++++++------ test/FileFormats/MOF/nonlinear.jl | 4 +- 7 files changed, 107 insertions(+), 72 deletions(-) diff --git a/Project.toml b/Project.toml index cae9938266..290e20a9e8 100644 --- a/Project.toml +++ b/Project.toml @@ -7,7 +7,6 @@ BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" CodecBzip2 = "523fee87-0ab8-5b00-afb7-3ecf72e48cfd" CodecZlib = "944b1d66-785c-5afd-91f1-9de20f533193" JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MutableArithmetics = "d8a4904e-b15c-11e9-3269-09a3773c0cb0" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" @@ -25,3 +24,9 @@ JSONSchema = "0.3" MutableArithmetics = "0.2" OrderedCollections = "1" julia = "1" + +[extras] +JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" + +[targets] +test = ["JSONSchema"] diff --git a/docs/Project.toml b/docs/Project.toml index f37abc4e24..b021fa903a 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,6 +1,10 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692" [compat] Documenter = "0.25" +JSON = "0.21" +JSONSchema = "0.3" diff --git a/docs/src/submodules/FileFormats/overview.md b/docs/src/submodules/FileFormats/overview.md index 64a56a2c48..6a245c8df7 100644 --- a/docs/src/submodules/FileFormats/overview.md +++ b/docs/src/submodules/FileFormats/overview.md @@ -201,3 +201,63 @@ A Mathematical Programming System (MPS) model julia> read!(io, src_2) ``` + +## Validating MOF files + +MathOptFormat files are governed by a schema. Use the [JSONSchema.jl](https://github.com/fredo-dedup/JSONSchema.jl) +package to check if a `.mof.json` file satisfies the schema. + +First, consturct the schema object as follows: +```jldoctest schema_mof +julia> import JSON, JSONSchema + +julia> schema = JSONSchema.Schema(JSON.parsefile(MOI.FileFormats.MOF.SCHEMA_PATH)) +A JSONSchema +``` + +Then, check if a model file is valid using `isvalid`: +```jldoctest schema_mof +julia> good_model = JSON.parse(""" + { + "version": { + "major": 0, + "minor": 5 + }, + "variables": [{"name": "x"}], + "objective": {"sense": "feasibility"}, + "constraints": [] + } + """); + +julia> isvalid(good_model, schema) +true +``` + +If we construct an invalid file, for example by mis-typing `name` as `NaMe`, the +validation fails: +```jldoctest schema_mof +julia> bad_model = JSON.parse(""" + { + "version": { + "major": 0, + "minor": 5 + }, + "variables": [{"NaMe": "x"}], + "objective": {"sense": "feasibility"}, + "constraints": [] + } + """); + +julia> isvalid(bad_model, schema) +false +``` + +Use `JSONSchema.validate` to obtain more insight into why the validation failed: +```jldoctest schema_mof +julia> JSONSchema.validate(bad_model, schema) +Validation failed: +path: [variables][1] +instance: Dict{String, Any}("NaMe" => "x") +schema key: required +schema value: Any["name"] +``` diff --git a/src/FileFormats/MOF/MOF.jl b/src/FileFormats/MOF/MOF.jl index 0db934f969..ebff1f4dcd 100644 --- a/src/FileFormats/MOF/MOF.jl +++ b/src/FileFormats/MOF/MOF.jl @@ -3,17 +3,12 @@ module MOF import ..FileFormats import OrderedCollections import JSON -import JSONSchema import MathOptInterface const MOI = MathOptInterface + const SCHEMA_PATH = joinpath(@__DIR__, "mof.0.6.schema.json") -const VERSION = let data = JSON.parsefile(SCHEMA_PATH, use_mmap = false) - VersionNumber( - data["properties"]["version"]["properties"]["major"]["const"], - data["properties"]["version"]["properties"]["minor"]["const"], - ) -end +const VERSION = v"0.6" const OrderedObject = OrderedCollections.OrderedDict{String,Any} const UnorderedObject = Dict{String,Any} @@ -75,12 +70,11 @@ const Model = MOI.Utilities.UniversalFallback{InnerModel{Float64}} struct Options print_compact::Bool - validate::Bool warn::Bool end function get_options(m::Model) - return get(m.model.ext, :MOF_OPTIONS, Options(false, false, false)) + return get(m.model.ext, :MOF_OPTIONS, Options(false, false)) end """ @@ -93,19 +87,14 @@ Keyword arguments are: - `print_compact::Bool=false`: print the JSON file in a compact format without spaces or newlines. - - `validate::Bool=false`: validate each file prior to reading against the MOF - schema. Defaults to `false` because this can take a long time for large - models. - - `warn::Bool=false`: print a warning when variables or constraints are renamed """ function Model(; print_compact::Bool = false, - validate::Bool = false, warn::Bool = false, ) model = MOI.Utilities.UniversalFallback(InnerModel{Float64}()) - model.model.ext[:MOF_OPTIONS] = Options(print_compact, validate, warn) + model.model.ext[:MOF_OPTIONS] = Options(print_compact, warn) return model end @@ -114,40 +103,6 @@ function Base.show(io::IO, ::Model) return end -""" - validate(filename::String) - -Validate that the MOF file `filename` conforms to the MOF JSON schema. Returns -`nothing` if the file is valid, otherwise throws an error describing why the -file is not valid. -""" -function validate(filename::String) - FileFormats.compressed_open( - filename, - "r", - FileFormats.AutomaticCompression(), - ) do io - return validate(io) - end - return -end - -function validate(io::IO) - object = JSON.parse(io) - seekstart(io) - mof_schema = - JSONSchema.Schema(JSON.parsefile(SCHEMA_PATH, use_mmap = false)) - ret = JSONSchema.validate(object, mof_schema) - if ret !== nothing - error( - "Unable to read file because it does not conform to the MOF " * - "schema: ", - ret, - ) - end - return -end - include("nonlinear.jl") include("read.jl") diff --git a/src/FileFormats/MOF/read.jl b/src/FileFormats/MOF/read.jl index 0bbe4a7c50..a7216ad9c3 100644 --- a/src/FileFormats/MOF/read.jl +++ b/src/FileFormats/MOF/read.jl @@ -7,10 +7,6 @@ function Base.read!(io::IO, model::Model) if !MOI.is_empty(model) error("Cannot read model from file as destination model is not empty.") end - options = get_options(model) - if options.validate - validate(io) - end object = JSON.parse(io; dicttype = UnorderedObject) file_version = _parse_mof_version(object["version"]::UnorderedObject) if file_version.major != VERSION.major || file_version.minor > VERSION.minor diff --git a/test/FileFormats/MOF/MOF.jl b/test/FileFormats/MOF/MOF.jl index 072e284b70..7e41beac7e 100644 --- a/test/FileFormats/MOF/MOF.jl +++ b/test/FileFormats/MOF/MOF.jl @@ -1,3 +1,5 @@ +import JSON +import JSONSchema import MathOptInterface using Test @@ -9,19 +11,40 @@ const TEST_MOF_FILE = "test.mof.json" @test sprint(show, MOF.Model()) == "A MathOptFormat Model" +const SCHEMA = + JSONSchema.Schema(JSON.parsefile(MOI.FileFormats.MOF.SCHEMA_PATH)) + +function _validate(filename::String) + MOI.FileFormats.compressed_open( + filename, + "r", + MOI.FileFormats.AutomaticCompression(), + ) do io + object = JSON.parse(io) + ret = JSONSchema.validate(object, SCHEMA) + if ret !== nothing + error( + "Unable to read file because it does not conform to the MOF " * + "schema: ", + ret, + ) + end + end +end + include("nonlinear.jl") struct UnsupportedSet <: MOI.AbstractSet end struct UnsupportedFunction <: MOI.AbstractFunction end function test_model_equality(model_string, variables, constraints; suffix = "") - model = MOF.Model(validate = true) + model = MOF.Model() MOIU.loadfromstring!(model, model_string) MOI.write_to_file(model, TEST_MOF_FILE * suffix) model_2 = MOF.Model() MOI.read_from_file(model_2, TEST_MOF_FILE * suffix) MOIU.test_models_equal(model, model_2, variables, constraints) - return MOF.validate(TEST_MOF_FILE * suffix) + return _validate(TEST_MOF_FILE * suffix) end @testset "Error handling: read_from_file" begin @@ -106,35 +129,27 @@ end @test MOF.moi_to_object(c1, model, name_map)["name"] == "c_1" @test MOF.moi_to_object(c2, model, name_map)["name"] == "c" end - @testset "v0.4" begin - filename = joinpath(@__DIR__, "v0.4.mof.json") - model = MOF.Model(validate = true) - @test_throws ErrorException MOI.read_from_file(model, filename) - model = MOF.Model(validate = false) - MOI.read_from_file(model, filename) - @test MOI.get(model, MOI.NumberOfVariables()) == 2 - end end @testset "round trips" begin @testset "Empty model" begin - model = MOF.Model(validate = true) + model = MOF.Model() MOI.write_to_file(model, TEST_MOF_FILE) - model_2 = MOF.Model(validate = true) + model_2 = MOF.Model() MOI.read_from_file(model_2, TEST_MOF_FILE) MOIU.test_models_equal(model, model_2, String[], String[]) end @testset "FEASIBILITY_SENSE" begin - model = MOF.Model(validate = true) + model = MOF.Model() x = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), x, "x") MOI.set(model, MOI.ObjectiveSense(), MOI.FEASIBILITY_SENSE) MOI.write_to_file(model, TEST_MOF_FILE) - model_2 = MOF.Model(validate = true) + model_2 = MOF.Model() MOI.read_from_file(model_2, TEST_MOF_FILE) MOIU.test_models_equal(model, model_2, ["x"], String[]) end @testset "Empty function term" begin - model = MOF.Model(validate = true) + model = MOF.Model() x = MOI.add_variable(model) MOI.set(model, MOI.VariableName(), x, "x") c = MOI.add_constraint( @@ -144,7 +159,7 @@ end ) MOI.set(model, MOI.ConstraintName(), c, "c") MOI.write_to_file(model, TEST_MOF_FILE) - model_2 = MOF.Model(validate = true) + model_2 = MOF.Model() MOI.read_from_file(model_2, TEST_MOF_FILE) MOIU.test_models_equal(model, model_2, ["x"], ["c"]) end diff --git a/test/FileFormats/MOF/nonlinear.jl b/test/FileFormats/MOF/nonlinear.jl index 58d9c8d4a5..9609e9b5d0 100644 --- a/test/FileFormats/MOF/nonlinear.jl +++ b/test/FileFormats/MOF/nonlinear.jl @@ -56,7 +56,7 @@ end read(joinpath(@__DIR__, "nlp.mof.json"), String), '\r' => "", ) - MOF.validate(TEST_MOF_FILE) + return _validate(TEST_MOF_FILE) end @testset "Error handling" begin node_list = MOF.Object[] @@ -188,6 +188,6 @@ end @test foo2.expr == :(2 * $x + sin($x)^2 - $y) @test MOI.get(model, MOI.ConstraintSet(), con) == MOI.get(model2, MOI.ConstraintSet(), con2) - MOF.validate(TEST_MOF_FILE) + return _validate(TEST_MOF_FILE) end end From 6ca31bea5910d8709ff924fd2a7ca55c508e82bf Mon Sep 17 00:00:00 2001 From: odow Date: Tue, 11 May 2021 15:15:50 +1200 Subject: [PATCH 2/3] Fix formatting and docs --- docs/src/submodules/FileFormats/overview.md | 6 +++--- src/FileFormats/MOF/MOF.jl | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/src/submodules/FileFormats/overview.md b/docs/src/submodules/FileFormats/overview.md index 6a245c8df7..6534e1ee1f 100644 --- a/docs/src/submodules/FileFormats/overview.md +++ b/docs/src/submodules/FileFormats/overview.md @@ -221,7 +221,7 @@ julia> good_model = JSON.parse(""" { "version": { "major": 0, - "minor": 5 + "minor": 6 }, "variables": [{"name": "x"}], "objective": {"sense": "feasibility"}, @@ -240,7 +240,7 @@ julia> bad_model = JSON.parse(""" { "version": { "major": 0, - "minor": 5 + "minor": 6 }, "variables": [{"NaMe": "x"}], "objective": {"sense": "feasibility"}, @@ -257,7 +257,7 @@ Use `JSONSchema.validate` to obtain more insight into why the validation failed: julia> JSONSchema.validate(bad_model, schema) Validation failed: path: [variables][1] -instance: Dict{String, Any}("NaMe" => "x") +instance: Dict{String,Any}("NaMe"=>"x") schema key: required schema value: Any["name"] ``` diff --git a/src/FileFormats/MOF/MOF.jl b/src/FileFormats/MOF/MOF.jl index ebff1f4dcd..6aa24518ac 100644 --- a/src/FileFormats/MOF/MOF.jl +++ b/src/FileFormats/MOF/MOF.jl @@ -89,10 +89,7 @@ Keyword arguments are: - `warn::Bool=false`: print a warning when variables or constraints are renamed """ -function Model(; - print_compact::Bool = false, - warn::Bool = false, -) +function Model(; print_compact::Bool = false, warn::Bool = false) model = MOI.Utilities.UniversalFallback(InnerModel{Float64}()) model.model.ext[:MOF_OPTIONS] = Options(print_compact, warn) return model From a00e16301ef7c116516972d7af1b820589f14d07 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Wed, 12 May 2021 08:27:37 +1200 Subject: [PATCH 3/3] Update overview.md --- docs/src/submodules/FileFormats/overview.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/submodules/FileFormats/overview.md b/docs/src/submodules/FileFormats/overview.md index 6534e1ee1f..f6ad498b88 100644 --- a/docs/src/submodules/FileFormats/overview.md +++ b/docs/src/submodules/FileFormats/overview.md @@ -204,10 +204,10 @@ julia> read!(io, src_2) ## Validating MOF files -MathOptFormat files are governed by a schema. Use the [JSONSchema.jl](https://github.com/fredo-dedup/JSONSchema.jl) -package to check if a `.mof.json` file satisfies the schema. +MathOptFormat files are governed by a schema. Use [JSONSchema.jl](https://github.com/fredo-dedup/JSONSchema.jl) +to check if a `.mof.json` file satisfies the schema. -First, consturct the schema object as follows: +First, construct the schema object as follows: ```jldoctest schema_mof julia> import JSON, JSONSchema