Browse Source

Merge pull request #76 from communitiesuk/CLDC-641-JSONValidation

Json Form Structure Validation against Schema
pull/88/head
Milo 3 years ago committed by GitHub
parent
commit
eae6f8f36e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      Gemfile
  2. 3
      Gemfile.lock
  3. 15
      README.md
  4. 112
      config/forms/schema/2021_2022.json
  5. 129
      config/forms/schema/generic.json
  6. 4
      db/schema.rb
  7. 50
      lib/tasks/form_definition.rake
  8. 946
      spec/fixtures/forms/test_form.json
  9. 48
      spec/fixtures/forms/test_validator.json

2
Gemfile

@ -29,6 +29,8 @@ gem "discard"
gem "activeadmin" gem "activeadmin"
# Admin charts # Admin charts
gem "chartkick" gem "chartkick"
#Json Schema
gem "json-schema"
gem "uk_postcode" gem "uk_postcode"
group :development, :test do group :development, :test do

3
Gemfile.lock

@ -196,6 +196,8 @@ GEM
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
json-schema (2.8.1)
addressable (>= 2.4)
kaminari (1.2.1) kaminari (1.2.1)
activesupport (>= 4.1.0) activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1) kaminari-actionview (= 1.2.1)
@ -399,6 +401,7 @@ DEPENDENCIES
govuk_design_system_formbuilder govuk_design_system_formbuilder
hotwire-rails hotwire-rails
jbuilder (~> 2.7) jbuilder (~> 2.7)
json-schema
listen (~> 3.3) listen (~> 3.3)
overcommit (>= 0.37.0) overcommit (>= 0.37.0)
pg (~> 1.1) pg (~> 1.1)

15
README.md

@ -120,6 +120,7 @@ The JSON should follow the structure:
"[snake_case_question_name_string]": { "[snake_case_question_name_string]": {
"header": String, "header": String,
"hint_text": String, "hint_text": String,
"check_answer_label": String,
"type": "text" / "numeric" / "radio" / "checkbox" / "date", "type": "text" / "numeric" / "radio" / "checkbox" / "date",
"min": Integer, // numeric only "min": Integer, // numeric only
"max": Integer, // numeric only "max": Integer, // numeric only
@ -133,6 +134,10 @@ The JSON should follow the structure:
"[snake_case_question_to_enable_2_name_string]": ["condition-that-enables"] "[snake_case_question_to_enable_2_name_string]": ["condition-that-enables"]
} }
} }
},
"conditional_route_to": {
"[page_name_to_route_to]": {"question_name": "expected_answer"},
"[page_name_to_route_to]": {"question_name": "expected_answer"}
} }
} }
} }
@ -155,6 +160,16 @@ Assumptions made by the format:
- Radio question answer option selected matches one of conditional e.g. ["answer-options-1-string", "answer-option-3-string"] - Radio question answer option selected matches one of conditional e.g. ["answer-options-1-string", "answer-option-3-string"]
- Numeric question value matches condition e.g. [">2"], ["<7"] or ["== 6"] - Numeric question value matches condition e.g. [">2"], ["<7"] or ["== 6"]
## JSON Form Validation against Schema
To validate the form JSON against the schema you can run:
`rake form_definition:validate`
This will validate all forms in:
directories = ["config/forms", "spec/fixtures/forms"]
against the schema in (config/forms/schema/generic.json)
## Useful documentation (external dependencies) ## Useful documentation (external dependencies)
### GOV.UK Design System Form Builder for Rails ### GOV.UK Design System Form Builder for Rails

112
config/forms/schema/2021_2022.json

@ -0,0 +1,112 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://example.com/product.schema.json",
"title": "Form",
"description": "A form",
"type": "object",
"required": ["form_type", "start_year", "end_year", "sections"],
"properties": {
"form_type": {
"description": "",
"type": "string"
},
"start_year": {
"description": "",
"type": "integer"
},
"end_year": {
"description": "",
"type": "integer"
},
"sections": {
"type": "object",
"patternProperties": {
"[a-z_]+": {
"description": "",
"type": "object",
"properties": {
"label": {
"description": "",
"type": "string"
},
"subsections": {
"type": "object",
"patternProperties": {
"[a-z_]+": {
"description": "",
"type": "object",
"required": ["label"],
"properties": {
"label": {
"description": "",
"type": "string"
},
"pages": {
"type": "object",
"patternProperties": {
"[a-z_]+": {
"description": "",
"type": "object",
"properties": {
"header": {
"description": "",
"type": "string"
},
"description": {
"description": "",
"type": "string"
},
"questions": {
"type": "object",
"patternProperties": {
"[a-z_]+": {
"description": "",
"type": "object",
"required": ["header", "check_answer_label"],
"properties": {
"check_answer_label": {
"description": "",
"type": "string"
},
"header": {
"description": "",
"type": "string"
},
"type": {
"description": "",
"type": "string"
},
"hint_text": {
"description": "",
"type": "string"
},
"answer_options": {
"description": "",
"type": "object"
},
"conditional_for": {
"description": "",
"type": "object"
}
}
}
}
}
},
"minProperties": 1
}
}
}
},
"minProperties": 1
}
}
}
},
"minProperties": 2
}
},
"minProperties": 1
}
}
}

129
config/forms/schema/generic.json

@ -0,0 +1,129 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$id": "https://example.com/product.schema.json",
"title": "Form",
"description": "A form",
"type": "object",
"required": ["form_type", "start_year", "end_year", "sections"],
"properties": {
"form_type": {
"description": "",
"type": "string"
},
"start_year": {
"description": "",
"type": "integer"
},
"end_year": {
"description": "",
"type": "integer"
},
"sections": {
"type": "object",
"patternProperties": {
"[a-z_]+": {
"description": "Section Name",
"type": "object",
"properties": {
"label": {
"description": "",
"type": "string"
},
"subsections": {
"type": "object",
"patternProperties": {
"[a-z_]+": {
"description": "SubSection Name",
"type": "object",
"required": ["label"],
"properties": {
"label": {
"description": "",
"type": "string"
},
"pages": {
"type": "object",
"patternProperties": {
"^(?!(conditional_route_to))[a-z_]+$": {
"description": "Page Name",
"type": "object",
"required": ["header", "questions"],
"properties": {
"header": {
"description": "",
"type": "string"
},
"description": {
"description": "",
"type": "string"
},
"questions": {
"type": "object",
"patternProperties": {
"[a-z_]+": {
"description": "Question Name",
"type": "object",
"required": ["header", "type"],
"properties": {
"header": {
"description": "",
"type": "string"
},
"type": {
"description": "",
"type": "string"
},
"check_answer_label": {
"description": "",
"type": "string",
"optional": "true"
}
},
"additionalProperties": {
"hint_text": {
"optional": "true",
"description": "",
"type": "string"
},
"answer_options": {
"optional": "true",
"description": "",
"type": "object"
},
"check_answer_label": {
"description": "",
"type": "string"
},
"conditional_for": {
"description": "",
"type": "object"
}
},
"minProperties": 1
}
}
}
},
"additionalProperties": {
"conditional_route_to": {
"description": "",
"type": "object"
}
},
"minProperties": 1
}
}
}
},
"minProperties": 1
}
}
}
},
"minProperties": 2
}
},
"minProperties": 1
}
}
}

4
db/schema.rb

@ -122,8 +122,6 @@ ActiveRecord::Schema.define(version: 2021_11_12_105348) do
t.integer "rp_dontknow" t.integer "rp_dontknow"
t.datetime "discarded_at" t.datetime "discarded_at"
t.string "tenancyother" t.string "tenancyother"
t.integer "override_net_income_validation"
t.string "net_income_known"
t.string "gdpr_acceptance" t.string "gdpr_acceptance"
t.string "gdpr_declined" t.string "gdpr_declined"
t.string "property_owner_organisation" t.string "property_owner_organisation"
@ -135,6 +133,8 @@ ActiveRecord::Schema.define(version: 2021_11_12_105348) do
t.string "needs_type" t.string "needs_type"
t.string "sale_completion_date" t.string "sale_completion_date"
t.string "purchaser_code" t.string "purchaser_code"
t.integer "override_net_income_validation"
t.string "net_income_known"
t.integer "reason" t.integer "reason"
t.string "propcode" t.string "propcode"
t.integer "majorrepairs" t.integer "majorrepairs"

50
lib/tasks/form_definition.rake

@ -0,0 +1,50 @@
require "json"
require "json-schema"
def get_all_form_paths(directories)
form_paths = []
directories.each do |directory|
Dir.glob("#{directory}/*.json").each do |form_path|
form_paths.push(form_path)
end
end
form_paths
end
namespace :form_definition do
desc "Validate JSON against Generic Form Schema"
task :validate do
puts "#{Rails.root}"
path = "config/forms/schema/generic.json"
file = File.open(path)
schema = JSON.parse(file.read)
metaschema = JSON::Validator.validator_for_name("draft4").metaschema
puts path
if JSON::Validator.validate(metaschema, schema)
puts "schema valid"
else
puts "schema not valid"
return
end
directories = ["config/forms", "spec/fixtures/forms"]
# directories = ["config/forms"]
get_all_form_paths(directories).each do |path|
puts path
file = File.open(path)
data = JSON.parse(file.read)
puts JSON::Validator.fully_validate(schema, data, :strict => true)
begin
JSON::Validator.validate!(schema, data)
rescue JSON::Schema::ValidationError => e
e.message
end
end
end
end

946
spec/fixtures/forms/test_form.json vendored

File diff suppressed because it is too large Load Diff

48
spec/fixtures/forms/test_validator.json vendored

@ -0,0 +1,48 @@
{
"form_type": "lettings",
"start_year": 2021,
"end_year": 2022,
"sections": {
"household": {
"label": "About the household",
"subsections": {
"household_characteristics": {
"label": "Household characteristics",
"ShouldThrowError": "Shouldn't be here but what you gonna do?",
"pages": {
"tenant_code": {
"header": "",
"description": "",
"ShouldThrowError": "Shouldn't be here but what you gonna do?",
"questions": {
"tenant_code": {
"check_answer_label": "Tenant code",
"header": "What is the tenant code?",
"description": "",
"type": "text"
}
},
"conditional_route_to": {"test": "Yes"}
},
"conditional_route_to": {"test": "Yes"},
"person_1_age": {
"header": "",
"description": "",
"questions": {
"person_1_age": {
"check_answer_label": "Tenant's age",
"header": "What is the tenant's age?",
"hint_text": "",
"type": "numeric",
"min": 0,
"max": 120,
"step": 1
}
}
}
}
}
}
}
}
}
Loading…
Cancel
Save