V4.3 - Advanced Validation

V4.3  Advanced Validation

added full gamut of options and configuration for fully fleshed out validation
This commit is contained in:
Joshua Hare 2025-08-29 00:11:24 +10:00
parent 356914f306
commit 92fe759b0f
5 changed files with 595 additions and 94 deletions

160
app.py
View File

@ -48,7 +48,9 @@ from services import (
import_manual_package_and_dependencies,
parse_and_list_igs,
generate_ig_yaml,
apply_and_validate
apply_and_validate,
run_validator_cli,
uuid
)
from forms import IgImportForm, ManualIgImportForm, ValidationForm, FSHConverterForm, TestDataUploadForm, RetrieveSplitDataForm, IgYamlForm
from wtforms import SubmitField
@ -70,11 +72,16 @@ app.config['FHIR_PACKAGES_DIR'] = os.path.join(instance_path, 'fhir_packages')
app.config['API_KEY'] = os.environ.get('API_KEY', 'your-fallback-api-key-here')
app.config['VALIDATE_IMPOSED_PROFILES'] = True
app.config['DISPLAY_PROFILE_RELATIONSHIPS'] = True
app.config['LATEST_BUILD_URL'] = "http://build.fhir.org/ig/qas.json"
app.config['VALIDATION_LOG_DIR'] = os.path.join(app.instance_path, 'validation_logs')
os.makedirs(app.config['VALIDATION_LOG_DIR'], exist_ok=True)
app.config['UPLOAD_FOLDER'] = os.path.join(CURRENT_DIR, 'static', 'uploads') # For GoFSH output
app.config['APP_BASE_URL'] = os.environ.get('APP_BASE_URL', 'http://localhost:5000')
app.config['HAPI_FHIR_URL'] = os.environ.get('HAPI_FHIR_URL', 'http://localhost:8080/fhir')
CONFIG_PATH = os.environ.get('CONFIG_PATH', '/usr/local/tomcat/conf/application.yaml')
# Basic Swagger configuration
app.config['SWAGGER'] = {
'title': 'FHIRFLARE IG Toolkit API',
@ -1765,6 +1772,157 @@ csrf.exempt(api_push_ig) # Add this line
# Exempt the entire API blueprint (for routes defined IN services.py, like /api/validate-sample)
csrf.exempt(services_bp) # Keep this line for routes defined in the blueprint
@app.route('/validate-sample', methods=['GET'])
def validate_resource_page():
"""Renders the validation form page."""
form = ValidationForm()
# Fetch list of packages to populate the dropdown
packages = current_app.config.get('MANUAL_PACKAGE_CACHE', [])
return render_template('validate_sample.html', form=form, packages=packages)
@app.route('/api/validate', methods=['POST'])
@swag_from({
'tags': ['Validation'],
'summary': 'Starts a validation process for a FHIR resource or bundle.',
'description': 'Starts a validation task and immediately returns a task ID for polling. The actual validation is run in a separate thread to prevent timeouts.',
'security': [{'ApiKeyAuth': []}],
'consumes': ['application/json'],
'parameters': [
{
'name': 'validation_payload',
'in': 'body',
'required': True,
'schema': {
'type': 'object',
'required': ['package_name', 'version', 'fhir_resource'],
'properties': {
'package_name': {'type': 'string', 'example': 'hl7.fhir.us.core'},
'version': {'type': 'string', 'example': '6.1.0'},
'fhir_resource': {'type': 'string', 'description': 'A JSON string of the FHIR resource or Bundle to validate.'},
'fhir_version': {'type': 'string', 'example': '4.0.1'},
'do_native': {'type': 'boolean', 'default': False},
'hint_about_must_support': {'type': 'boolean', 'default': False},
'assume_valid_rest_references': {'type': 'boolean', 'default': False},
'no_extensible_binding_warnings': {'type': 'boolean', 'default': False},
'show_times': {'type': 'boolean', 'default': False},
'allow_example_urls': {'type': 'boolean', 'default': False},
'check_ips_codes': {'type': 'boolean', 'default': False},
'allow_any_extensions': {'type': 'boolean', 'default': False},
'tx_routing': {'type': 'boolean', 'default': False},
'snomed_ct_version': {'type': 'string', 'default': ''},
'profiles': {'type': 'string', 'description': 'Comma-separated list of profile URLs.'},
'extensions': {'type': 'string', 'description': 'Comma-separated list of extension URLs.'},
'terminology_server': {'type': 'string', 'description': 'URL of an alternate terminology server.'}
}
}
}
],
'responses': {
'202': {'description': 'Validation process started successfully, returns a task_id.'},
'400': {'description': 'Invalid request (e.g., missing fields, invalid JSON).'}
}
})
def validate_resource():
"""API endpoint to run validation on a FHIR resource."""
try:
# Get data from the JSON payload
data = request.get_json()
if not data:
return jsonify({'status': 'error', 'message': 'Invalid JSON: No data received.'}), 400
fhir_resource_text = data.get('fhir_resource')
fhir_version = data.get('fhir_version')
package_name = data.get('package_name')
version = data.get('version')
profiles = data.get('profiles')
extensions = data.get('extensions')
terminology_server = data.get('terminology_server')
snomed_ct_version = data.get('snomed_ct_version')
# Build the list of IGs from the package name and version
igs = [f"{package_name}#{version}"] if package_name and version else []
flags = {
'doNative': data.get('do_native'),
'hintAboutMustSupport': data.get('hint_about_must_support'),
'assumeValidRestReferences': data.get('assume_valid_rest_references'),
'noExtensibleBindingWarnings': data.get('no_extensible_binding_warnings'),
'showTimes': data.get('show_times'),
'allowExampleUrls': data.get('allow_example_urls'),
'checkIpsCodes': data.get('check_ips_codes'),
'allowAnyExtensions': data.get('allow_any_extensions'),
'txRouting': data.get('tx_routing'),
}
# Check for empty resource
if not fhir_resource_text:
return jsonify({'status': 'error', 'message': 'No FHIR resource provided.'}), 400
try:
# Generate a unique task ID
task_id = str(uuid.uuid4())
# Store a pending status in the global cache
with services.cache_lock:
services.validation_results_cache[task_id] = {"status": "pending"}
# Start the validation process in a new thread
app_context = app.app_context()
thread = threading.Thread(
target=services.validate_in_thread,
args=(app_context, task_id, fhir_resource_text, fhir_version, igs, profiles, extensions, terminology_server, flags, snomed_ct_version),
daemon=True
)
thread.start()
logger.info(f"Validation task {task_id} started in background thread.")
return jsonify({"status": "accepted", "message": "Validation started", "task_id": task_id}), 202
except Exception as e:
logger.error(f"Error starting validation thread: {e}", exc_info=True)
return jsonify({'status': 'error', 'message': f'An internal server error occurred while starting the validation: {e}'}), 500
except Exception as e:
logger.error(f"Validation API error: {e}")
return jsonify({'status': 'error', 'message': f'An internal server error occurred: {e}'}), 500
@app.route('/api/check-validation-status/<task_id>', methods=['GET'])
@swag_from({
'tags': ['Validation'],
'summary': 'Check the status of a validation task.',
'description': 'Polls for the result of a validation task by its ID. Returns the full validation report when the task is complete.',
'security': [{'ApiKeyAuth': []}],
'parameters': [
{'name': 'task_id', 'in': 'path', 'type': 'string', 'required': True, 'description': 'The unique ID of the validation task.'}
],
'responses': {
'200': {'description': 'Returns the full validation report if the task is complete.'},
'202': {'description': 'The validation task is still in progress.'},
'404': {'description': 'The specified task ID was not found.'}
}
})
def check_validation_status(task_id):
"""
This GET endpoint is polled by the frontend to check the status of a validation task.
"""
with services.cache_lock:
task_result = services.validation_results_cache.get(task_id)
if task_result is None:
return jsonify({"status": "error", "message": "Task not found."}), 404
if task_result["status"] == "pending":
return jsonify({"status": "pending", "message": "Validation is still in progress."}), 202
# Remove the result from the cache after it has been retrieved to prevent a memory leak
with services.cache_lock:
del services.validation_results_cache[task_id]
return jsonify({"status": "complete", "result": task_result["result"]}), 200
def create_db():
logger.debug(f"Attempting to create database tables for URI: {app.config['SQLALCHEMY_DATABASE_URI']}")
try:

View File

@ -17,5 +17,5 @@ services:
- NODE_PATH=/usr/lib/node_modules
- APP_MODE=standalone
- APP_BASE_URL=http://localhost:5000
- HAPI_FHIR_URL=https://fhir.hl7.org.au/aucore/fhir/DEFAULT/
- HAPI_FHIR_URL=https://smile.sparked-fhir.com/aucore/fhir/DEFAULT/
command: supervisord -c /etc/supervisord.conf

View File

@ -312,3 +312,58 @@ class IgYamlForm(FlaskForm):
"""Form to select IGs for YAML generation."""
igs = SelectMultipleField('Select IGs to include', validators=[DataRequired()])
generate_yaml = SubmitField('Generate YAML')
# --- New ValidationForm class for the new page ---
class ValidationForm(FlaskForm):
"""Form for validating a single FHIR resource with various options."""
# Added fields to match the HTML template
package_name = StringField('Package Name', validators=[Optional()])
version = StringField('Package Version', validators=[Optional()])
# Main content fields
fhir_resource = TextAreaField('FHIR Resource (JSON or XML)', validators=[InputRequired()],
render_kw={'placeholder': 'Paste your FHIR JSON here...'})
fhir_version = SelectField('FHIR Version', choices=[
('4.0.1', 'R4 (4.0.1)'),
('3.0.2', 'STU3 (3.0.2)'),
('5.0.0', 'R5 (5.0.0)')
], default='4.0.1', validators=[DataRequired()])
# Flags and options from validator settings.pdf
do_native = BooleanField('Native Validation (doNative)')
hint_about_must_support = BooleanField('Must Support (hintAboutMustSupport)')
assume_valid_rest_references = BooleanField('Assume Valid Rest References (assumeValidRestReferences)')
no_extensible_binding_warnings = BooleanField('Extensible Binding Warnings (noExtensibleBindingWarnings)')
show_times = BooleanField('Show Times (-show-times)')
allow_example_urls = BooleanField('Allow Example URLs (-allow-example-urls)')
check_ips_codes = BooleanField('Check IPS Codes (-check-ips-codes)')
allow_any_extensions = BooleanField('Allow Any Extensions (-allow-any-extensions)')
tx_routing = BooleanField('Show Terminology Routing (-tx-routing)')
# SNOMED CT options
snomed_ct_version = SelectField('Select SNOMED Version', choices=[
('', '-- No selection --'),
('intl', 'International edition (900000000000207008)'),
('us', 'US edition (731000124108)'),
('uk', 'United Kingdom Edition (999000041000000102)'),
('es', 'Spanish Language Edition (449081005)'),
('nl', 'Netherlands Edition (11000146104)'),
('ca', 'Canadian Edition (20611000087101)'),
('dk', 'Danish Edition (554471000005108)'),
('se', 'Swedish Edition (45991000052106)'),
('au', 'Australian Edition (32506021000036107)'),
('be', 'Belgium Edition (11000172109)')
], validators=[Optional()])
# Text input fields
profiles = StringField('Profiles',
validators=[Optional()],
render_kw={'placeholder': 'e.g. http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient'})
extensions = StringField('Extensions',
validators=[Optional()],
render_kw={'placeholder': 'e.g. http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace'})
terminology_server = StringField('Terminology Server',
validators=[Optional()],
render_kw={'placeholder': 'e.g. http://tx.fhir.org'})
submit = SubmitField('Validate')

View File

@ -198,6 +198,203 @@ def safe_parse_version(v_str):
logger.error(f"Unexpected error in safe_parse_version for '{v_str}': {e}")
return pkg_version.parse("0.0.0a0") # Fallback
# --- New Function to run the FHIR Validator CLI ---
def run_validator_cli(resource_text, fhir_version, igs, profiles, extensions, terminology_server, flags, snomed_ct_version, app_config):
"""
Constructs and runs the Java FHIR Validator CLI command.
Args:
resource_text (str): The FHIR resource/bundle as a string.
fhir_version (str): The FHIR version to validate against.
igs (list): List of selected IG canonical URLs.
profiles (str): Comma-separated list of profile URLs.
extensions (str): Comma-separated list of extension URLs.
terminology_server (str): The URL of the terminology server.
flags (dict): Dictionary of boolean flags for validation.
snomed_ct_version (str): The SNOMED CT version to use.
app_config (dict): The Flask application configuration.
Returns:
dict: A dictionary containing the validation output and status.
"""
temp_dir = tempfile.mkdtemp(dir=app_config['VALIDATION_LOG_DIR'])
# Store the temp directory in the app config for the download endpoint to find it.
app_config['VALIDATION_TEMP_DIR'] = temp_dir
temp_resource_path = os.path.join(temp_dir, f'resource_{uuid.uuid4()}.json')
try:
with open(temp_resource_path, 'w', encoding='utf-8') as f:
f.write(resource_text)
except Exception as e:
shutil.rmtree(temp_dir)
return {'status': 'error', 'message': f'Failed to write temporary resource file: {e}'}
output_json_path = os.path.join(temp_dir, 'validation-report.json')
output_html_path = os.path.join(temp_dir, 'validation-report.html')
command = [
'java',
'-jar', os.environ.get('JAVA_VALIDATOR_PATH', '/app/validator_cli/validator_cli.jar'),
temp_resource_path,
'-version', fhir_version,
'-output', output_json_path,
'-html-output', output_html_path,
]
# Add flags
if flags.get('doNative'):
command.append('-do-native')
if flags.get('hintAboutMustSupport'):
command.append('-hint-about-must-support')
if flags.get('assumeValidRestReferences'):
command.append('-assume-valid-rest-references')
if flags.get('noExtensibleBindingWarnings'):
command.append('-no-extensible-binding-warnings')
if flags.get('showTimes'):
command.append('-show-times')
if flags.get('allowExampleUrls'):
command.append('-allow-example-urls')
if flags.get('checkIpsCodes'):
command.append('-check-ips-codes')
if flags.get('allowAnyExtensions'):
command.append('-allow-any-extensions')
if flags.get('txRouting'):
command.append('-tx-routing')
# Add other options
if igs:
for ig in igs:
command.extend(['-ig', ig])
if profiles:
command.extend(['-profile', profiles])
if extensions:
command.extend(['-extension', extensions])
if terminology_server:
command.extend(['-tx', terminology_server])
if snomed_ct_version:
command.extend(['-sct', snomed_ct_version])
logger.info(f"Running validator with command: {' '.join(command)}")
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
check=False,
timeout=300,
)
stdout = result.stdout
stderr = result.stderr
logger.debug(f"Validator stdout:\n{stdout}")
logger.debug(f"Validator stderr:\n{stderr}")
# Check for errors in the stdout and stderr
if result.returncode != 0 and not os.path.exists(output_json_path):
return_dict = {
'status': 'error',
'output': stderr or "Validation process failed with an unknown error.",
'file_paths': {},
'errors': [stderr or "Validation process failed with an unknown error."],
'warnings': [],
'summary': {'error_count': 1, 'warning_count': 0},
'results': {}
}
return return_dict
# Now, try to read the JSON file produced by the validator
if os.path.exists(output_json_path):
with open(output_json_path, 'r', encoding='utf-8') as f:
json_report = json.load(f)
# Process the JSON report to get a structured summary
error_count = 0
warning_count = 0
errors_list = []
warnings_list = []
detailed_results = {}
is_valid = True
for issue in json_report.get('issue', []):
severity = issue.get('severity')
diagnostics = issue.get('diagnostics') or issue.get('details', {}).get('text') or 'No details provided.'
resource_expression = issue.get('expression', ['unknown'])[0]
# Determine the resource ID and type from the expression, e.g., "AllergyIntolerance" or "AllergyIntolerance[0].extension[0]"
resource_id = resource_expression.split('[')[0].split('.')[0]
if resource_id not in detailed_results:
detailed_results[resource_id] = {'valid': True, 'details': []}
detail = {
'issue': diagnostics,
'severity': severity,
'description': issue.get('details', {}).get('text', diagnostics)
}
detailed_results[resource_id]['details'].append(detail)
if severity == 'error':
error_count += 1
errors_list.append(diagnostics)
is_valid = False
detailed_results[resource_id]['valid'] = False
elif severity == 'warning':
warning_count += 1
warnings_list.append(diagnostics)
# Determine the overall status
status = 'success'
if error_count > 0:
status = 'error'
elif warning_count > 0:
status = 'warning'
# Build the final return dictionary
return_dict = {
'status': status,
'valid': is_valid,
'output': stdout,
'file_paths': {
'json': output_json_path,
'html': output_html_path
},
'errors': errors_list,
'warnings': warnings_list,
'summary': {
'error_count': error_count,
'warning_count': warning_count
},
'results': detailed_results
}
return return_dict
else:
# Fallback if validator completed but didn't produce a file
return_dict = {
'status': 'error',
'valid': False,
'output': stdout,
'file_paths': {},
'errors': ["Validator failed to produce the expected output files."],
'warnings': [],
'summary': {'error_count': 1, 'warning_count': 0},
'results': {}
}
return return_dict
except FileNotFoundError:
return {'status': 'error', 'message': 'Java command or validator JAR not found. Please check paths.'}
except subprocess.TimeoutExpired:
return {'status': 'error', 'message': 'Validation timed out.'}
except Exception as e:
return {'status': 'error', 'message': f'An unexpected error occurred during validation: {e}'}
finally:
# NOTE: We no longer delete the temporary directory here.
# It is the responsibility of the `clear-validation-results` endpoint.
pass
# --- MODIFIED FUNCTION with Enhanced Logging ---
def get_additional_registries():
"""Fetches the list of additional FHIR IG registries from the master feed."""
@ -1108,15 +1305,16 @@ validation_results_cache = {}
# Thread lock to safely manage access to the cache
cache_lock = threading.Lock()
def validate_in_thread(app_context, task_id, package_name, version, sample_data):
def validate_in_thread(app_context, task_id, fhir_resource_text, fhir_version, igs, profiles, extensions, terminology_server, flags, snomed_ct_version):
"""
Wrapper function to run the blocking validator in a separate thread.
It acquires an app context and then calls the main validation logic.
"""
with app_context:
logger.info(f"Starting validation thread for task ID: {task_id}")
# Call the core blocking validation function
result = run_java_validator(package_name, version, sample_data)
# Call the core blocking validation function, passing all arguments
result = run_validator_cli(fhir_resource_text, fhir_version, igs, profiles, extensions, terminology_server, flags, snomed_ct_version, current_app.config)
# Safely update the shared cache with the final result
with cache_lock:

View File

@ -22,8 +22,9 @@
<div class="card">
<div class="card-header">Validation Form</div>
<div class="card-body">
<form id="validationForm">
{{ form.hidden_tag() }}
<form id="validationForm" method="POST" action="/api/validate">
{{ form.csrf_token }}
<div class="mb-3">
<small class="form-text text-muted">
Select a package from the list below or enter a new one (e.g., <code>hl7.fhir.us.core</code>).
@ -46,6 +47,7 @@
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<div class="mb-3">
<label for="{{ form.version.id }}" class="form-label">Package Version</label>
{{ form.version(class="form-control", id=form.version.id) }}
@ -53,36 +55,111 @@
<div class="text-danger">{{ error }}</div>
{% endfor %}
</div>
<hr class="my-4">
<!-- New Fields from ValidationForm -->
<div class="mb-3">
<label for="{{ form.include_dependencies.id }}" class="form-label">Include Dependencies</label>
<div class="form-check">
{{ form.include_dependencies(class="form-check-input") }}
{{ form.include_dependencies.label(class="form-check-label") }}
<label for="fhir_resource" class="form-label">FHIR Resource (JSON or XML)</label>
{{ form.fhir_resource(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
</div>
<div class="row g-3">
<div class="col-md-6">
<label for="fhir_version" class="form-label">FHIR Version</label>
{{ form.fhir_version(class="form-select") }}
</div>
</div>
<div class="mb-3">
<label for="{{ form.mode.id }}" class="form-label">Validation Mode</label>
{{ form.mode(class="form-select") }}
<hr class="my-4">
<h5>Validation Flags</h5>
<div class="row g-3">
<div class="col-md-6">
<div class="form-check">
{{ form.do_native(class="form-check-input") }}
<label class="form-check-label" for="do_native">{{ form.do_native.label }}</label>
</div>
<div class="form-check">
{{ form.hint_about_must_support(class="form-check-input") }}
<label class="form-check-label" for="hint_about_must_support">{{ form.hint_about_must_support.label }}</label>
</div>
<div class="form-check">
{{ form.assume_valid_rest_references(class="form-check-input") }}
<label class="form-check-label" for="assume_valid_rest_references">{{ form.assume_valid_rest_references.label }}</label>
</div>
<div class="form-check">
{{ form.no_extensible_binding_warnings(class="form-check-input") }}
<label class="form-check-label" for="no_extensible_binding_warnings">{{ form.no_extensible_binding_warnings.label }}</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
{{ form.show_times(class="form-check-input") }}
<label class="form-check-label" for="show_times">{{ form.show_times.label }}</label>
</div>
<div class="form-check">
{{ form.allow_example_urls(class="form-check-input") }}
<label class="form-check-label" for="allow_example_urls">{{ form.allow_example_urls.label }}</label>
</div>
<div class="form-check">
{{ form.check_ips_codes(class="form-check-input") }}
<label class="form-check-label" for="check_ips_codes">{{ form.check_ips_codes.label }}</label>
</div>
<div class="form-check">
{{ form.allow_any_extensions(class="form-check-input") }}
<label class="form-check-label" for="allow_any_extensions">{{ form.allow_any_extensions.label }}</label>
</div>
<div class="form-check">
{{ form.tx_routing(class="form-check-input") }}
<label class="form-check-label" for="tx_routing">{{ form.tx_routing.label }}</label>
</div>
</div>
</div>
<div class="mb-3">
<label for="{{ form.sample_input.id }}" class="form-label">Sample FHIR Resource/Bundle (JSON)</label>
{{ form.sample_input(class="form-control", rows=10, placeholder="Paste your FHIR JSON here...") }}
{% for error in form.sample_input.errors %}
<div class="text-danger">{{ error }}</div>
{% endfor %}
<hr class="my-4">
<h5>Other Settings</h5>
<div class="row g-3">
<div class="col-md-6">
<label for="profiles" class="form-label">{{ form.profiles.label }}</label>
{{ form.profiles(class="form-control") }}
<small class="form-text text-muted">Comma-separated URLs, e.g., url1,url2</small>
</div>
<div class="col-md-6">
<label for="extensions" class="form-label">{{ form.extensions.label }}</label>
{{ form.extensions(class="form-control") }}
<small class="form-text text-muted">Comma-separated URLs, e.g., url1,url2</small>
</div>
<div class="col-md-6">
<label for="terminology_server" class="form-label">{{ form.terminology_server.label }}</label>
{{ form.terminology_server(class="form-control") }}
<small class="form-text text-muted">e.g., http://tx.fhir.org</small>
</div>
<div class="col-md-6">
<label for="snomed_ct_version" class="form-label">{{ form.snomed_ct_version.label }}</label>
{{ form.snomed_ct_version(class="form-select") }}
<small class="form-text text-muted">Select a SNOMED CT edition.</small>
</div>
</div>
<div class="d-flex justify-content-between">
<button type="button" class="btn btn-primary" id="validateButton">Validate</button>
<button type="button" class="btn btn-secondary d-none" id="clearResultsButton">Clear Results</button>
<div class="mt-4 text-center">
<button type="submit" id="validateButton" class="btn btn-primary btn-lg">
Validate Resource
</button>
<button type="button" id="clearResultsButton" class="btn btn-secondary btn-lg d-none">
Clear Results
</button>
</div>
</form>
</div>
</div>
<div class="card mt-4" id="validationResult" style="display: none;">
<div class="card-header">Validation Report</div>
<div id="validationResult" class="card mt-4" style="display: none;">
<div class="card-header">Validation Results</div>
<div class="card-body" id="validationContent">
</div>
<!-- Results will be displayed here -->
</div>
</div>
</div>
@ -94,6 +171,7 @@
const spinnerOverlay = document.getElementById('spinner-overlay');
const spinnerMessage = document.getElementById('spinner-message');
const spinnerContainer = document.getElementById('spinner-animation');
const perPage = 10;
// Function to load the Lottie animation based on theme
function loadLottieAnimation(theme) {
@ -136,17 +214,18 @@
// --- Frontend logic starts here
document.addEventListener('DOMContentLoaded', function() {
const packageSelect = document.getElementById('packageSelect');
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
const versionInput = document.getElementById('{{ form.version.id }}');
const form = document.getElementById('validationForm');
const validateButton = document.getElementById('validateButton');
const clearResultsButton = document.getElementById('clearResultsButton');
const sampleInput = document.getElementById('{{ form.sample_input.id }}');
const validationResult = document.getElementById('validationResult');
const validationContent = document.getElementById('validationContent');
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
const perPage = 10;
const packageSelect = document.getElementById('packageSelect');
const packageNameInput = document.getElementById('{{ form.package_name.id }}');
const versionInput = document.getElementById('{{ form.version.id }}');
const fhirResourceInput = document.getElementById('fhir_resource');
// Function to update package fields when a package is selected from the dropdown
function updatePackageFields(select) {
const value = select.value;
if (!packageNameInput || !versionInput) return;
@ -165,37 +244,7 @@
}
}
if (packageNameInput.value && versionInput.value) {
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
const optionExists = Array.from(packageSelect.options).some(opt => opt.value === currentValue);
if (optionExists) {
packageSelect.value = currentValue;
}
}
if (packageSelect) {
packageSelect.addEventListener('change', function() {
updatePackageFields(this);
});
}
if (sampleInput) {
sampleInput.addEventListener('input', function() {
try {
if (this.value.trim()) {
JSON.parse(this.value);
this.classList.remove('is-invalid');
this.classList.add('is-valid');
} else {
this.classList.remove('is-valid', 'is-invalid');
}
} catch (e) {
this.classList.remove('is-valid');
this.classList.add('is-invalid');
}
});
}
// Sanitize text to prevent XSS
function sanitizeText(text) {
const div = document.createElement('div');
div.textContent = text;
@ -226,7 +275,7 @@
li.innerHTML = `
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
${isWarnings && item.includes('Must Support') ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
${isWarnings && (item.issue && item.issue.includes('Must Support')) ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
</div>
`;
ul.appendChild(li);
@ -246,7 +295,7 @@
li.innerHTML = `
<div class="alert alert-${isWarnings ? 'warning' : 'danger'}">
${sanitizeText(typeof item === 'string' ? item : item.message || item.issue)}
${isWarnings && item.includes('Must Support') ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
${isWarnings && (item.issue && item.issue.includes('Must Support')) ? ' <i class="bi bi-info-circle" title="This element is marked as Must Support in the profile."></i>' : ''}
</div>
`;
remainingUl.appendChild(li);
@ -275,7 +324,7 @@
clearResultsButton.classList.remove('d-none');
if (data.file_paths) {
// Fix: Extract just the filename to construct the download URL
// Use the paths returned from the server to create download links
const jsonFileName = data.file_paths.json.split('/').pop();
const htmlFileName = data.file_paths.html.split('/').pop();
const downloadDiv = document.createElement('div');
@ -398,10 +447,11 @@
});
}
function runBlockingValidation() {
const packageName = packageNameInput.value;
const version = versionInput.value;
const sampleData = sampleInput.value.trim();
function runBlockingValidation(e) {
e.preventDefault(); // This is the crucial line to prevent default form submission
const packageName = document.getElementById('{{ form.package_name.id }}').value;
const version = document.getElementById('{{ form.version.id }}').value;
const sampleData = document.getElementById('fhir_resource').value.trim();
const csrfToken = document.querySelector('input[name="csrf_token"]').value;
if (!packageName || !version) {
@ -412,31 +462,40 @@
if (!sampleData) {
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON.</div>';
return;
}
try {
JSON.parse(sampleData);
} catch (e) {
validationResult.style.display = 'block';
validationContent.innerHTML = '<div class="alert alert-danger">Invalid JSON: ' + sanitizeText(e.message) + '</div>';
validationContent.innerHTML = '<div class="alert alert-danger">Please provide FHIR sample JSON or XML.</div>';
return;
}
toggleSpinner(true, "Running FHIR Validator (This may take up to 5 minutes on the first run).");
fetch('/api/validate-sample', {
// Collect all form data including the new fields
const data = {
package_name: packageName,
version: version,
fhir_resource: sampleData,
fhir_version: document.getElementById('fhir_version').value,
do_native: document.getElementById('do_native').checked,
hint_about_must_support: document.getElementById('hint_about_must_support').checked,
assume_valid_rest_references: document.getElementById('assume_valid_rest_references').checked,
no_extensible_binding_warnings: document.getElementById('no_extensible_binding_warnings').checked,
show_times: document.getElementById('show_times').checked,
allow_example_urls: document.getElementById('allow_example_urls').checked,
check_ips_codes: document.getElementById('check_ips_codes').checked,
allow_any_extensions: document.getElementById('allow_any_extensions').checked,
tx_routing: document.getElementById('tx_routing').checked,
snomed_ct_version: document.getElementById('snomed_ct_version').value,
profiles: document.getElementById('profiles').value,
extensions: document.getElementById('extensions').value,
terminology_server: document.getElementById('terminology_server').value
};
fetch('/api/validate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
package_name: packageName,
version: version,
sample_data: sampleData
})
body: JSON.stringify(data)
})
.then(response => {
if (response.status === 202) {
@ -462,9 +521,9 @@
toggleSpinner(false);
console.error('Validation failed:', error);
validationResult.style.display = 'block';
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred: ${sanitizeText(error.message)}</div>`;
validationContent.innerHTML = `<div class="alert alert-danger">An unexpected error occurred during validation: ${sanitizeText(error.message)}</div>`;
});
}
};
clearResultsButton.addEventListener('click', function() {
fetch('/api/clear-validation-results', {
@ -487,9 +546,40 @@
});
});
if (packageSelect) {
packageSelect.addEventListener('change', function() {
updatePackageFields(this);
});
}
if (validateButton) {
validateButton.addEventListener('click', function() {
runBlockingValidation();
validateButton.addEventListener('click', function(e) {
runBlockingValidation(e);
});
}
if (packageNameInput.value && versionInput.value) {
const currentValue = `${packageNameInput.value}#${versionInput.value}`;
const optionExists = Array.from(packageSelect.options).some(opt => opt.value === currentValue);
if (optionExists) {
packageSelect.value = currentValue;
}
}
if (fhirResourceInput) {
fhirResourceInput.addEventListener('input', function() {
try {
if (this.value.trim()) {
JSON.parse(this.value);
this.classList.remove('is-invalid');
this.classList.add('is-valid');
} else {
this.classList.remove('is-valid', 'is-invalid');
}
} catch (e) {
this.classList.remove('is-valid');
this.classList.add('is-invalid');
}
});
}
});