From faf61c4b81ff3443ddbae1bf7fc5d438b92344b0 Mon Sep 17 00:00:00 2001 From: hamrt Date: Thu, 12 Sep 2024 00:20:34 +0200 Subject: [PATCH 01/45] changes for extra custommapping --- .vscode/launch.json | 16 + example_ELM-to-OBv3.json | 1062 +++++++++++ example_OBv3-to-ELM.json | 1062 +++++++++++ ...omp Generic.json => DigiComp_Generic.json} | 0 ...redential_Data_and_software_business.json} | 0 json/mapping/custom_mapping.json | 179 +- json/mapping/custom_mapping_ELM_OBv3.json | 310 ++++ .../mapping/custom_mapping_ELM_OBv3_copy.json | 287 +++ json/output_credential.json | 1601 ++++++++++++++++- src/backend/candidate_value.rs | 21 +- src/backend/repository.rs | 32 +- src/backend/transformations.rs | 26 + src/render/mapping_bars.rs | 1 + src/render/p2.rs | 1 + src/render/p3.rs | 1 + src/state.rs | 2 + 16 files changed, 4594 insertions(+), 7 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 example_ELM-to-OBv3.json create mode 100644 example_OBv3-to-ELM.json rename json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/{DigiComp Generic.json => DigiComp_Generic.json} (100%) rename json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/{Microcredential - Data and software business.json => Microcredential_Data_and_software_business.json} (100%) create mode 100644 json/mapping/custom_mapping_ELM_OBv3.json create mode 100644 json/mapping/custom_mapping_ELM_OBv3_copy.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..10efcb2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug", + "program": "${workspaceFolder}/", + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/example_ELM-to-OBv3.json b/example_ELM-to-OBv3.json new file mode 100644 index 0000000..0d60eb3 --- /dev/null +++ b/example_ELM-to-OBv3.json @@ -0,0 +1,1062 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "http://data.europa.eu/snb/model/context/edc-ap" + ], + "awardedDate": "2024-03-26T16:06:50+01:00", + "awardeddate": "2019-09-20T00:00:00+02:00", + "credentialSchema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialSubject": { + "achievement": { + "criteria": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "hasPart": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + }, + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "urn:epass:learningAchievement:2", + "provenBy": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "grade": { + "id": "urn:epass:note:2", + "noteLiteral": { + "en": [ + "10" + ] + }, + "type": "Note" + }, + "id": "urn:epass:learningAssessment:1", + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:1", + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessmentSpecification" + }, + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessment" + } + ], + "specifiedBy": { + "eqfLevel": { + "id": "http://data.europa.eu/snb/eqf/5", + "inScheme": { + "id": "http://data.europa.eu/snb/eqf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "Level 5" + ] + }, + "type": "Concept" + }, + "id": "urn:epass:qualification:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:2", + "relatedSkill": [ + { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + }, + { + "id": "urn:epass:learningOutcome:3", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/34v10n662m", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "3.1 Proficiency Level Foundation 2" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence 2" + ] + }, + "type": "LearningOutcome" + } + ], + "learningOutcomeSummary": { + "id": "urn:epass:note:3", + "noteLiteral": { + "en": [ + "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" + ] + }, + "type": "Note" + }, + "title": { + "en": [ + "Title of Achievement" + ] + }, + "type": "Qualification" + }, + "title": { + "en": [ + "TITLE OF PROGRAMME" + ] + }, + "type": "LearningAchievement" + } + ], + "description": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "hasPart": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + }, + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "urn:epass:learningAchievement:2", + "provenBy": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "grade": { + "id": "urn:epass:note:2", + "noteLiteral": { + "en": [ + "10" + ] + }, + "type": "Note" + }, + "id": "urn:epass:learningAssessment:1", + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:1", + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessmentSpecification" + }, + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessment" + } + ], + "specifiedBy": { + "eqfLevel": { + "id": "http://data.europa.eu/snb/eqf/5", + "inScheme": { + "id": "http://data.europa.eu/snb/eqf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "Level 5" + ] + }, + "type": "Concept" + }, + "id": "urn:epass:qualification:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:2", + "relatedSkill": [ + { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + }, + { + "id": "urn:epass:learningOutcome:3", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/34v10n662m", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "3.1 Proficiency Level Foundation 2" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence 2" + ] + }, + "type": "LearningOutcome" + } + ], + "learningOutcomeSummary": { + "id": "urn:epass:note:3", + "noteLiteral": { + "en": [ + "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" + ] + }, + "type": "Note" + }, + "title": { + "en": [ + "Title of Achievement" + ] + }, + "type": "Qualification" + }, + "title": { + "en": [ + "TITLE OF PROGRAMME" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "did:key:afsdlkj34134", + "name": [ + "David Smith" + ], + "type": "Person" + }, + "type": [ + { + "id": "urn:epass:identifier:2", + "notation": "545465468", + "schemeName": "Student ID", + "type": "Identifier" + } + ] + }, + "description": [ + "EBSI Example https://github.com/Knowledge-Innovation-Centre/ESBI-JSON-schemas/blob/main/examples%20of%20credentials/DigiComp%20Generic.json" + ], + "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", + "issuancedate": "2024-03-26T16:06:50+01:00", + "issuer": "urn:epass:identifier:2", + "name": "credentialSubjectje", + "type": [ + "VerifiableCredential", + "VerifiableAttestation", + "EuropeanDigitalCredential" + ], + "validFrom": "2019-09-20T00:00:00+02:00" +} \ No newline at end of file diff --git a/example_OBv3-to-ELM.json b/example_OBv3-to-ELM.json new file mode 100644 index 0000000..0fecb4f --- /dev/null +++ b/example_OBv3-to-ELM.json @@ -0,0 +1,1062 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "http://data.europa.eu/snb/model/context/edc-ap" + ], + "awardedDate": "2024-03-26T16:06:50+01:00", + "awardeddate": "2019-09-20T00:00:00+02:00", + "credentialSchema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialSubject": { + "achievement": { + "criteria": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "hasPart": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + }, + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "urn:epass:learningAchievement:2", + "provenBy": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "grade": { + "id": "urn:epass:note:2", + "noteLiteral": { + "en": [ + "10" + ] + }, + "type": "Note" + }, + "id": "urn:epass:learningAssessment:1", + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:1", + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessmentSpecification" + }, + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessment" + } + ], + "specifiedBy": { + "eqfLevel": { + "id": "http://data.europa.eu/snb/eqf/5", + "inScheme": { + "id": "http://data.europa.eu/snb/eqf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "Level 5" + ] + }, + "type": "Concept" + }, + "id": "urn:epass:qualification:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:2", + "relatedSkill": [ + { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + }, + { + "id": "urn:epass:learningOutcome:3", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/34v10n662m", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "3.1 Proficiency Level Foundation 2" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence 2" + ] + }, + "type": "LearningOutcome" + } + ], + "learningOutcomeSummary": { + "id": "urn:epass:note:3", + "noteLiteral": { + "en": [ + "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" + ] + }, + "type": "Note" + }, + "title": { + "en": [ + "Title of Achievement" + ] + }, + "type": "Qualification" + }, + "title": { + "en": [ + "TITLE OF PROGRAMME" + ] + }, + "type": "LearningAchievement" + } + ], + "description": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "hasPart": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + }, + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "urn:epass:learningAchievement:2", + "provenBy": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "grade": { + "id": "urn:epass:note:2", + "noteLiteral": { + "en": [ + "10" + ] + }, + "type": "Note" + }, + "id": "urn:epass:learningAssessment:1", + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:1", + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessmentSpecification" + }, + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessment" + } + ], + "specifiedBy": { + "eqfLevel": { + "id": "http://data.europa.eu/snb/eqf/5", + "inScheme": { + "id": "http://data.europa.eu/snb/eqf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "Level 5" + ] + }, + "type": "Concept" + }, + "id": "urn:epass:qualification:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:2", + "relatedSkill": [ + { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + }, + { + "id": "urn:epass:learningOutcome:3", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/34v10n662m", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "3.1 Proficiency Level Foundation 2" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence 2" + ] + }, + "type": "LearningOutcome" + } + ], + "learningOutcomeSummary": { + "id": "urn:epass:note:3", + "noteLiteral": { + "en": [ + "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" + ] + }, + "type": "Note" + }, + "title": { + "en": [ + "Title of Achievement" + ] + }, + "type": "Qualification" + }, + "title": { + "en": [ + "TITLE OF PROGRAMME" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "did:key:afsdlkj34134", + "name": [ + "David Smith" + ], + "type": "Person" + }, + "type": [ + { + "id": "urn:epass:identifier:2", + "notation": "545465468", + "schemeName": "Student ID", + "type": "Identifier" + } + ] + }, + "credentialsubject": "credentialSubjectje", + "description": [ + "EBSI Example https://github.com/Knowledge-Innovation-Centre/ESBI-JSON-schemas/blob/main/examples%20of%20credentials/DigiComp%20Generic.json" + ], + "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", + "issuancedate": "2024-03-26T16:06:50+01:00", + "issuer": "urn:epass:identifier:2", + "type": [ + "VerifiableCredential", + "VerifiableAttestation", + "EuropeanDigitalCredential" + ], + "validFrom": "2019-09-20T00:00:00+02:00" +} \ No newline at end of file diff --git a/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp Generic.json b/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic.json similarity index 100% rename from json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp Generic.json rename to json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic.json diff --git a/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/Microcredential - Data and software business.json b/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/Microcredential_Data_and_software_business.json similarity index 100% rename from json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/Microcredential - Data and software business.json rename to json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/Microcredential_Data_and_software_business.json diff --git a/json/mapping/custom_mapping.json b/json/mapping/custom_mapping.json index 0637a08..7bac0ef 100644 --- a/json/mapping/custom_mapping.json +++ b/json/mapping/custom_mapping.json @@ -1 +1,178 @@ -[] \ No newline at end of file +[ + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.@context" + }, + "destination": { + "format": "OBv3", + "path": "$.@context" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.familyName.en" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.type.items" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.type" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.type.contains" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSchema" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialschema" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialStatus" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialstatus" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialsubject" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.evidence" + }, + "destination": { + "format": "OBv3", + "path": "$.evidence" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuanceDate" + }, + "destination": { + "format": "OBv3", + "path": "$.issuancedate" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuer" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.proof" + }, + "destination": { + "format": "OBv3", + "path": "$.proof" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.termsOfUse" + }, + "destination": { + "format": "OBv3", + "path": "$.termsofuse" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.validFrom" + }, + "destination": { + "format": "OBv3", + "path": "$.awardeddate" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.validUntil" + }, + "destination": { + "format": "OBv3", + "path": "$.expirationdate" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.refreshService" + }, + "destination": { + "format": "OBv3", + "path": "$.refreshservice" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.validFrom" + }, + "destination": { + "format": "OBv3", + "path": "$.validFrom" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.id" + }, + "destination": { + "format": "OBv3", + "path": "$.id" + } + } +] \ No newline at end of file diff --git a/json/mapping/custom_mapping_ELM_OBv3.json b/json/mapping/custom_mapping_ELM_OBv3.json new file mode 100644 index 0000000..46eccb3 --- /dev/null +++ b/json/mapping/custom_mapping_ELM_OBv3.json @@ -0,0 +1,310 @@ +[ + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSchema" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialschema" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialStatus" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialstatus" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialsubject" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.evidence" + }, + "destination": { + "format": "OBv3", + "path": "$.evidence" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuanceDate" + }, + "destination": { + "format": "OBv3", + "path": "$.issuancedate" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuer" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.proof" + }, + "destination": { + "format": "OBv3", + "path": "$.proof" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.termsOfUse" + }, + "destination": { + "format": "OBv3", + "path": "$.termsofuse" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.validFrom" + }, + "destination": { + "format": "OBv3", + "path": "$.awardeddate" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.validUntil" + }, + "destination": { + "format": "OBv3", + "path": "$.expirationdate" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.refreshService" + }, + "destination": { + "format": "OBv3", + "path": "$.refreshservice" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.id" + }, + "destination": { + "format": "OBv3", + "path": "$.id" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.@context" + }, + "destination": { + "format": "OBv3", + "path": "$.@context" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.id" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.id" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.description" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.fullName.en" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.name" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.type" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.type" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.identifier" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.type" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.criteria" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuer.id" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuer.identifier.type" + }, + "destination": { + "format": "OBv3", + "path": "$.type" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.validFrom" + }, + "destination": { + "format": "OBv3", + "path": "$.validFrom" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSchema" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSchema" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuer.identifier.id" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.type" + }, + "destination": { + "format": "OBv3", + "path": "$.type" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.displayParameter.description.en" + }, + "destination": { + "format": "OBv3", + "path": "$.description" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.displayParameter.description.en" + }, + "destination": { + "format": "OBv3", + "path": "$.description" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuanceDate" + }, + "destination": { + "format": "OBv3", + "path": "$.awardedDate" + } + } +] \ No newline at end of file diff --git a/json/mapping/custom_mapping_ELM_OBv3_copy.json b/json/mapping/custom_mapping_ELM_OBv3_copy.json new file mode 100644 index 0000000..4d20058 --- /dev/null +++ b/json/mapping/custom_mapping_ELM_OBv3_copy.json @@ -0,0 +1,287 @@ +[ + { + "type_": "stringit", + "source": { + "value": "credentialSubjectje" + }, + "destination": { + "format": "OBv3", + "path": "$.name" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.evidence" + }, + "destination": { + "format": "OBv3", + "path": "$.evidence" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuanceDate" + }, + "destination": { + "format": "OBv3", + "path": "$.issuancedate" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuer" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.proof" + }, + "destination": { + "format": "OBv3", + "path": "$.proof" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.termsOfUse" + }, + "destination": { + "format": "OBv3", + "path": "$.termsofuse" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.validFrom" + }, + "destination": { + "format": "OBv3", + "path": "$.awardeddate" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.validUntil" + }, + "destination": { + "format": "OBv3", + "path": "$.expirationdate" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.refreshService" + }, + "destination": { + "format": "OBv3", + "path": "$.refreshservice" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.id" + }, + "destination": { + "format": "OBv3", + "path": "$.id" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.@context" + }, + "destination": { + "format": "OBv3", + "path": "$.@context" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.id" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.id" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.description" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.fullName.en" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.name" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.type" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.type" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.identifier" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.type" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.criteria" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuer.id" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuer.identifier.type" + }, + "destination": { + "format": "OBv3", + "path": "$.type" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.validFrom" + }, + "destination": { + "format": "OBv3", + "path": "$.validFrom" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSchema" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSchema" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuer.identifier.id" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.type" + }, + "destination": { + "format": "OBv3", + "path": "$.type" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.displayParameter.description.en" + }, + "destination": { + "format": "OBv3", + "path": "$.description" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.displayParameter.description.en" + }, + "destination": { + "format": "OBv3", + "path": "$.description" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuanceDate" + }, + "destination": { + "format": "OBv3", + "path": "$.awardedDate" + } + } +] \ No newline at end of file diff --git a/json/output_credential.json b/json/output_credential.json index abeba65..e3af0db 100644 --- a/json/output_credential.json +++ b/json/output_credential.json @@ -2,5 +2,1604 @@ "@context": [ "https://www.w3.org/ns/credentials/v2", "http://data.europa.eu/snb/model/context/edc-ap" - ] + ], + "awardedDate": "2024-03-26T16:06:50+01:00", + "awardeddate": "2019-09-20T00:00:00+02:00", + "credentialSchema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialSubject": { + "achievement": { + "criteria": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "hasPart": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + }, + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "urn:epass:learningAchievement:2", + "provenBy": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "grade": { + "id": "urn:epass:note:2", + "noteLiteral": { + "en": [ + "10" + ] + }, + "type": "Note" + }, + "id": "urn:epass:learningAssessment:1", + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:1", + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessmentSpecification" + }, + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessment" + } + ], + "specifiedBy": { + "eqfLevel": { + "id": "http://data.europa.eu/snb/eqf/5", + "inScheme": { + "id": "http://data.europa.eu/snb/eqf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "Level 5" + ] + }, + "type": "Concept" + }, + "id": "urn:epass:qualification:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:2", + "relatedSkill": [ + { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + }, + { + "id": "urn:epass:learningOutcome:3", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/34v10n662m", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "3.1 Proficiency Level Foundation 2" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence 2" + ] + }, + "type": "LearningOutcome" + } + ], + "learningOutcomeSummary": { + "id": "urn:epass:note:3", + "noteLiteral": { + "en": [ + "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" + ] + }, + "type": "Note" + }, + "title": { + "en": [ + "Title of Achievement" + ] + }, + "type": "Qualification" + }, + "title": { + "en": [ + "TITLE OF PROGRAMME" + ] + }, + "type": "LearningAchievement" + } + ], + "description": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "hasPart": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + }, + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "urn:epass:learningAchievement:2", + "provenBy": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "grade": { + "id": "urn:epass:note:2", + "noteLiteral": { + "en": [ + "10" + ] + }, + "type": "Note" + }, + "id": "urn:epass:learningAssessment:1", + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:1", + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessmentSpecification" + }, + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessment" + } + ], + "specifiedBy": { + "eqfLevel": { + "id": "http://data.europa.eu/snb/eqf/5", + "inScheme": { + "id": "http://data.europa.eu/snb/eqf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "Level 5" + ] + }, + "type": "Concept" + }, + "id": "urn:epass:qualification:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:2", + "relatedSkill": [ + { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + }, + { + "id": "urn:epass:learningOutcome:3", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/34v10n662m", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "3.1 Proficiency Level Foundation 2" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence 2" + ] + }, + "type": "LearningOutcome" + } + ], + "learningOutcomeSummary": { + "id": "urn:epass:note:3", + "noteLiteral": { + "en": [ + "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" + ] + }, + "type": "Note" + }, + "title": { + "en": [ + "Title of Achievement" + ] + }, + "type": "Qualification" + }, + "title": { + "en": [ + "TITLE OF PROGRAMME" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "did:key:afsdlkj34134", + "name": [ + "David Smith" + ], + "type": "Person" + }, + "type": [ + { + "id": "urn:epass:identifier:2", + "notation": "545465468", + "schemeName": "Student ID", + "type": "Identifier" + } + ] + }, + "credentialschema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialsubject": { + "familyName": { + "en": [ + "Smith" + ] + }, + "fullName": { + "en": [ + "David Smith" + ] + }, + "givenName": { + "en": [ + "David" + ] + }, + "hasClaim": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "hasPart": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + }, + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "urn:epass:learningAchievement:2", + "provenBy": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "grade": { + "id": "urn:epass:note:2", + "noteLiteral": { + "en": [ + "10" + ] + }, + "type": "Note" + }, + "id": "urn:epass:learningAssessment:1", + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:1", + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessmentSpecification" + }, + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessment" + } + ], + "specifiedBy": { + "eqfLevel": { + "id": "http://data.europa.eu/snb/eqf/5", + "inScheme": { + "id": "http://data.europa.eu/snb/eqf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "Level 5" + ] + }, + "type": "Concept" + }, + "id": "urn:epass:qualification:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:2", + "relatedSkill": [ + { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + }, + { + "id": "urn:epass:learningOutcome:3", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/34v10n662m", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "3.1 Proficiency Level Foundation 2" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence 2" + ] + }, + "type": "LearningOutcome" + } + ], + "learningOutcomeSummary": { + "id": "urn:epass:note:3", + "noteLiteral": { + "en": [ + "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" + ] + }, + "type": "Note" + }, + "title": { + "en": [ + "Title of Achievement" + ] + }, + "type": "Qualification" + }, + "title": { + "en": [ + "TITLE OF PROGRAMME" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "did:key:afsdlkj34134", + "identifier": [ + { + "id": "urn:epass:identifier:2", + "notation": "545465468", + "schemeName": "Student ID", + "type": "Identifier" + } + ], + "type": "Person" + }, + "description": [ + "EBSI Example https://github.com/Knowledge-Innovation-Centre/ESBI-JSON-schemas/blob/main/examples%20of%20credentials/DigiComp%20Generic.json" + ], + "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", + "issuancedate": "2024-03-26T16:06:50+01:00", + "issuer": "urn:epass:identifier:2", + "type": [ + "VerifiableCredential", + "VerifiableAttestation", + "EuropeanDigitalCredential" + ], + "validFrom": "2019-09-20T00:00:00+02:00" } \ No newline at end of file diff --git a/src/backend/candidate_value.rs b/src/backend/candidate_value.rs index b3b9257..7ba7cc1 100644 --- a/src/backend/candidate_value.rs +++ b/src/backend/candidate_value.rs @@ -3,12 +3,13 @@ use regex::Regex; use crate::{ backend::{ jsonpointer::{JsonPath, JsonPointer}, - transformations::{DataLocation, OneToOne, Transformation}, + transformations::{DataLocation, OneToOne, StringToOne, StringValue, Transformation}, }, state::{AppState, Pages, Transformations}, trace_dbg, }; + pub fn set_candidate_output_value(state: &mut AppState, push_transformation: bool) { // todo: is it needed to add directcopy to the start? let selected_transformations = [ @@ -44,10 +45,10 @@ pub fn set_candidate_output_value(state: &mut AppState, push_transformation: boo pub fn define_transformation(state: &mut AppState, transformation: Transformations) -> Transformation { let (input_format, output_format) = (state.mapping.input_format(), state.mapping.output_format()); let source_pointer: JsonPath = JsonPointer(state.input_fields[state.selected_input_field].0.clone()).into(); - + let input_value: String = state.input_fields[state.selected_input_field].0.clone(); let destination_path: JsonPath = JsonPointer(state.output_pointer.clone()).into(); - match transformation { + let transformation = match transformation { Transformations::LowerCase => Transformation::OneToOne { type_: OneToOne::toLowerCase, source: DataLocation { @@ -72,6 +73,16 @@ pub fn define_transformation(state: &mut AppState, transformation: Transformatio }, // todo: This clippy warning is known, this body is for 'DirectCopy' and all others until they // get their own branches + Transformations::StringToOne => Transformation::StringToOne { + type_: StringToOne::stringit, + source: StringValue { + value: input_value.clone() + }, + destination: DataLocation { + format: output_format.clone(), + path: destination_path.to_string(), + }, + }, Transformations::DirectCopy | _ => Transformation::OneToOne { type_: OneToOne::copy, source: DataLocation { @@ -83,7 +94,9 @@ pub fn define_transformation(state: &mut AppState, transformation: Transformatio path: destination_path.to_string(), }, }, - } + }; + let transformation = transformation; + transformation } pub fn set_output_pointer(state: &mut AppState) { diff --git a/src/backend/repository.rs b/src/backend/repository.rs index c2135b3..a18ccfc 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -1,7 +1,7 @@ use crate::{ backend::{ jsonpointer::{JsonPath, JsonPointer}, - transformations::{DataLocation, Transformation}, + transformations::{DataLocation, StringValue, Transformation}, }, state::{AppState, Mapping}, trace_dbg, @@ -117,6 +117,36 @@ impl Repository { merge(destination_credential, leaf_node); } + Transformation::StringToOne { + type_: transformation, + source: + StringValue { + value: source_value, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if destination_format != mapping.output_format() { + return; + } + + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path)).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(source_value); + } + + merge(destination_credential, leaf_node); + } + + _ => todo!(), } trace_dbg!("Successfully completed transformation"); diff --git a/src/backend/transformations.rs b/src/backend/transformations.rs index 9db8f46..9494c1b 100644 --- a/src/backend/transformations.rs +++ b/src/backend/transformations.rs @@ -61,6 +61,22 @@ impl ManyToOne { } } +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum StringToOne { + stringit, +} + +impl StringToOne { + pub fn apply(&self, value: String) -> Value { + match self { + StringToOne::stringit => Value::String(value), + } + } +} + + + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum Transformation { @@ -69,6 +85,11 @@ pub enum Transformation { source: DataLocation, destination: DataLocation, }, + StringToOne { + type_: StringToOne, + source: StringValue, + destination: DataLocation, + }, OneToMany { type_: OneToMany, source: DataLocation, @@ -86,3 +107,8 @@ pub struct DataLocation { pub format: String, pub path: String, } + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct StringValue { + pub value: String, +} \ No newline at end of file diff --git a/src/render/mapping_bars.rs b/src/render/mapping_bars.rs index 4a9627e..38b5dc5 100644 --- a/src/render/mapping_bars.rs +++ b/src/render/mapping_bars.rs @@ -38,6 +38,7 @@ pub fn render_mapping_bar(bottom: Rect, buf: &mut Buffer, state: &mut AppState, match state.mapping_option { MappingOptions::Transformations => render_transformations_bar(bottom, buf, state), MappingOptions::OneToMany => render_onetomany_bar(bottom, buf, state), + MappingOptions::StringToOne => render_onetomany_bar(bottom, buf, state), MappingOptions::ManyToOne => render_manytoone_bar(bottom, buf, state), MappingOptions::DirectCopy => {} } diff --git a/src/render/p2.rs b/src/render/p2.rs index 647e6e2..8a64091 100644 --- a/src/render/p2.rs +++ b/src/render/p2.rs @@ -158,6 +158,7 @@ pub fn render_required_data_p2(area: Rect, buf: &mut Buffer, state: &mut AppStat match state.mapping_option { MappingOptions::Transformations => render_popup_mapping(area, buf, state), MappingOptions::OneToMany => render_popup_mapping(area, buf, state), // todo + MappingOptions::StringToOne => render_popup_mapping(area, buf, state), // todo MappingOptions::ManyToOne => render_manytoone_bar(area, buf, state), // todo MappingOptions::DirectCopy => {} // DirectCopy } diff --git a/src/render/p3.rs b/src/render/p3.rs index 541ce78..2f49ea6 100644 --- a/src/render/p3.rs +++ b/src/render/p3.rs @@ -174,6 +174,7 @@ pub fn render_optional_data_p3(area: Rect, buf: &mut Buffer, state: &mut AppStat match state.mapping_option { MappingOptions::Transformations => render_popup_mapping(area, buf, state), MappingOptions::OneToMany => render_popup_mapping(area, buf, state), //todo + MappingOptions::StringToOne => render_popup_mapping(area, buf, state), //todo MappingOptions::ManyToOne => render_manytoone_bar(area, buf, state), //todo MappingOptions::DirectCopy => {} // DirectCopy } diff --git a/src/state.rs b/src/state.rs index 350e35d..36759b8 100644 --- a/src/state.rs +++ b/src/state.rs @@ -179,6 +179,7 @@ pub enum MappingOptions { Transformations, OneToMany, ManyToOne, + StringToOne, } #[derive(Clone, Copy, FromRepr, Debug, Default, PartialEq, Display)] pub enum Transformations { @@ -188,6 +189,7 @@ pub enum Transformations { UpperCase, Slice, Regex, + StringToOne, } #[derive(Clone, Copy, FromRepr, Debug, Default, PartialEq)] From baea7c60a87d07a86b5badada0e0cf4315276c4a Mon Sep 17 00:00:00 2001 From: hamrt Date: Sat, 14 Sep 2024 16:32:36 +0200 Subject: [PATCH 02/45] first work on mapping --- .vscode/settings.json | 5 + example_ELM-to-OBv3.json | 1060 +---------- .../custom_mapping_ELM_OBv3_latest.json | 104 ++ json/output_credential.json | 1603 +---------------- json/output_credential_ELM2OBv3.json | 574 ++++++ 5 files changed, 693 insertions(+), 2653 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 json/mapping/custom_mapping_ELM_OBv3_latest.json create mode 100644 json/output_credential_ELM2OBv3.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b242572 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "githubPullRequests.ignoredPullRequestBranches": [ + "main" + ] +} \ No newline at end of file diff --git a/example_ELM-to-OBv3.json b/example_ELM-to-OBv3.json index 0d60eb3..1d04c66 100644 --- a/example_ELM-to-OBv3.json +++ b/example_ELM-to-OBv3.json @@ -1,1058 +1,14 @@ { - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "http://data.europa.eu/snb/model/context/edc-ap" - ], - "awardedDate": "2024-03-26T16:06:50+01:00", - "awardeddate": "2019-09-20T00:00:00+02:00", - "credentialSchema": [ - { - "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", - "type": "ShaclValidator2017" - }, - { - "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", - "type": "JsonSchema" + "@context": "http://data.europa.eu/snb/model/context/edc-ap", + "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", + "issuer": { + "id": "urn:epass:org:1", + "name": "University of Life", + "type": { + "[0]": "Profile" } - ], - "credentialSubject": { - "achievement": { - "criteria": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "hasPart": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - }, - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - } - ], - "id": "urn:epass:learningAchievement:2", - "provenBy": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "grade": { - "id": "urn:epass:note:2", - "noteLiteral": { - "en": [ - "10" - ] - }, - "type": "Note" - }, - "id": "urn:epass:learningAssessment:1", - "specifiedBy": { - "id": "urn:epass:learningAssessmentSpec:1", - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessmentSpecification" - }, - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessment" - } - ], - "specifiedBy": { - "eqfLevel": { - "id": "http://data.europa.eu/snb/eqf/5", - "inScheme": { - "id": "http://data.europa.eu/snb/eqf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "Level 5" - ] - }, - "type": "Concept" - }, - "id": "urn:epass:qualification:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:2", - "relatedSkill": [ - { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - }, - { - "id": "urn:epass:learningOutcome:3", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/34v10n662m", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "3.1 Proficiency Level Foundation 2" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence 2" - ] - }, - "type": "LearningOutcome" - } - ], - "learningOutcomeSummary": { - "id": "urn:epass:note:3", - "noteLiteral": { - "en": [ - "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" - ] - }, - "type": "Note" - }, - "title": { - "en": [ - "Title of Achievement" - ] - }, - "type": "Qualification" - }, - "title": { - "en": [ - "TITLE OF PROGRAMME" - ] - }, - "type": "LearningAchievement" - } - ], - "description": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "hasPart": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - }, - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - } - ], - "id": "urn:epass:learningAchievement:2", - "provenBy": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "grade": { - "id": "urn:epass:note:2", - "noteLiteral": { - "en": [ - "10" - ] - }, - "type": "Note" - }, - "id": "urn:epass:learningAssessment:1", - "specifiedBy": { - "id": "urn:epass:learningAssessmentSpec:1", - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessmentSpecification" - }, - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessment" - } - ], - "specifiedBy": { - "eqfLevel": { - "id": "http://data.europa.eu/snb/eqf/5", - "inScheme": { - "id": "http://data.europa.eu/snb/eqf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "Level 5" - ] - }, - "type": "Concept" - }, - "id": "urn:epass:qualification:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:2", - "relatedSkill": [ - { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - }, - { - "id": "urn:epass:learningOutcome:3", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/34v10n662m", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "3.1 Proficiency Level Foundation 2" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence 2" - ] - }, - "type": "LearningOutcome" - } - ], - "learningOutcomeSummary": { - "id": "urn:epass:note:3", - "noteLiteral": { - "en": [ - "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" - ] - }, - "type": "Note" - }, - "title": { - "en": [ - "Title of Achievement" - ] - }, - "type": "Qualification" - }, - "title": { - "en": [ - "TITLE OF PROGRAMME" - ] - }, - "type": "LearningAchievement" - } - ], - "id": "did:key:afsdlkj34134", - "name": [ - "David Smith" - ], - "type": "Person" - }, - "type": [ - { - "id": "urn:epass:identifier:2", - "notation": "545465468", - "schemeName": "Student ID", - "type": "Identifier" - } - ] }, - "description": [ - "EBSI Example https://github.com/Knowledge-Innovation-Centre/ESBI-JSON-schemas/blob/main/examples%20of%20credentials/DigiComp%20Generic.json" - ], - "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", - "issuancedate": "2024-03-26T16:06:50+01:00", - "issuer": "urn:epass:identifier:2", - "name": "credentialSubjectje", + "name": "TITLE OF PROGRAMME", "type": [ "VerifiableCredential", "VerifiableAttestation", diff --git a/json/mapping/custom_mapping_ELM_OBv3_latest.json b/json/mapping/custom_mapping_ELM_OBv3_latest.json new file mode 100644 index 0000000..53c8ca9 --- /dev/null +++ b/json/mapping/custom_mapping_ELM_OBv3_latest.json @@ -0,0 +1,104 @@ +[ + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.@context" + }, + "destination": { + "format": "OBv3", + "path": "$.@context" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.id" + }, + "destination": { + "format": "OBv3", + "path": "$.id" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.type" + }, + "destination": { + "format": "OBv3", + "path": "$.type" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].awardedBy.awardingBody[0].id" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer.id" + } + }, + { + "type_": "stringit", + "source": { + "value": "Profile" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer.type.[0]" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].awardedBy.awardingBody[0].legalName.en[0]" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer.name" + } + }, + { + "type_": { + "takeIndex": { + "index": 1 + } + }, + "source": { + "format": "ELM", + "path": "$.@context" + }, + "destination": { + "format": "OBv3", + "path": "$.@context" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.validFrom" + }, + "destination": { + "format": "OBv3", + "path": "$.validFrom" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].title.en[0]" + }, + "destination": { + "format": "OBv3", + "path": "$.name" + } + } +] \ No newline at end of file diff --git a/json/output_credential.json b/json/output_credential.json index e3af0db..9169f44 100644 --- a/json/output_credential.json +++ b/json/output_credential.json @@ -1,1605 +1,6 @@ { "@context": [ "https://www.w3.org/ns/credentials/v2", - "http://data.europa.eu/snb/model/context/edc-ap" - ], - "awardedDate": "2024-03-26T16:06:50+01:00", - "awardeddate": "2019-09-20T00:00:00+02:00", - "credentialSchema": [ - { - "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", - "type": "ShaclValidator2017" - }, - { - "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", - "type": "JsonSchema" - } - ], - "credentialSubject": { - "achievement": { - "criteria": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "hasPart": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - }, - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - } - ], - "id": "urn:epass:learningAchievement:2", - "provenBy": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "grade": { - "id": "urn:epass:note:2", - "noteLiteral": { - "en": [ - "10" - ] - }, - "type": "Note" - }, - "id": "urn:epass:learningAssessment:1", - "specifiedBy": { - "id": "urn:epass:learningAssessmentSpec:1", - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessmentSpecification" - }, - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessment" - } - ], - "specifiedBy": { - "eqfLevel": { - "id": "http://data.europa.eu/snb/eqf/5", - "inScheme": { - "id": "http://data.europa.eu/snb/eqf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "Level 5" - ] - }, - "type": "Concept" - }, - "id": "urn:epass:qualification:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:2", - "relatedSkill": [ - { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - }, - { - "id": "urn:epass:learningOutcome:3", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/34v10n662m", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "3.1 Proficiency Level Foundation 2" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence 2" - ] - }, - "type": "LearningOutcome" - } - ], - "learningOutcomeSummary": { - "id": "urn:epass:note:3", - "noteLiteral": { - "en": [ - "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" - ] - }, - "type": "Note" - }, - "title": { - "en": [ - "Title of Achievement" - ] - }, - "type": "Qualification" - }, - "title": { - "en": [ - "TITLE OF PROGRAMME" - ] - }, - "type": "LearningAchievement" - } - ], - "description": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "hasPart": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - }, - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - } - ], - "id": "urn:epass:learningAchievement:2", - "provenBy": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "grade": { - "id": "urn:epass:note:2", - "noteLiteral": { - "en": [ - "10" - ] - }, - "type": "Note" - }, - "id": "urn:epass:learningAssessment:1", - "specifiedBy": { - "id": "urn:epass:learningAssessmentSpec:1", - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessmentSpecification" - }, - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessment" - } - ], - "specifiedBy": { - "eqfLevel": { - "id": "http://data.europa.eu/snb/eqf/5", - "inScheme": { - "id": "http://data.europa.eu/snb/eqf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "Level 5" - ] - }, - "type": "Concept" - }, - "id": "urn:epass:qualification:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:2", - "relatedSkill": [ - { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - }, - { - "id": "urn:epass:learningOutcome:3", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/34v10n662m", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "3.1 Proficiency Level Foundation 2" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence 2" - ] - }, - "type": "LearningOutcome" - } - ], - "learningOutcomeSummary": { - "id": "urn:epass:note:3", - "noteLiteral": { - "en": [ - "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" - ] - }, - "type": "Note" - }, - "title": { - "en": [ - "Title of Achievement" - ] - }, - "type": "Qualification" - }, - "title": { - "en": [ - "TITLE OF PROGRAMME" - ] - }, - "type": "LearningAchievement" - } - ], - "id": "did:key:afsdlkj34134", - "name": [ - "David Smith" - ], - "type": "Person" - }, - "type": [ - { - "id": "urn:epass:identifier:2", - "notation": "545465468", - "schemeName": "Student ID", - "type": "Identifier" - } - ] - }, - "credentialschema": [ - { - "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", - "type": "ShaclValidator2017" - }, - { - "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", - "type": "JsonSchema" - } - ], - "credentialsubject": { - "familyName": { - "en": [ - "Smith" - ] - }, - "fullName": { - "en": [ - "David Smith" - ] - }, - "givenName": { - "en": [ - "David" - ] - }, - "hasClaim": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "hasPart": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - }, - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - } - ], - "id": "urn:epass:learningAchievement:2", - "provenBy": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "grade": { - "id": "urn:epass:note:2", - "noteLiteral": { - "en": [ - "10" - ] - }, - "type": "Note" - }, - "id": "urn:epass:learningAssessment:1", - "specifiedBy": { - "id": "urn:epass:learningAssessmentSpec:1", - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessmentSpecification" - }, - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessment" - } - ], - "specifiedBy": { - "eqfLevel": { - "id": "http://data.europa.eu/snb/eqf/5", - "inScheme": { - "id": "http://data.europa.eu/snb/eqf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "Level 5" - ] - }, - "type": "Concept" - }, - "id": "urn:epass:qualification:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:2", - "relatedSkill": [ - { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - }, - { - "id": "urn:epass:learningOutcome:3", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/34v10n662m", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "3.1 Proficiency Level Foundation 2" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence 2" - ] - }, - "type": "LearningOutcome" - } - ], - "learningOutcomeSummary": { - "id": "urn:epass:note:3", - "noteLiteral": { - "en": [ - "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" - ] - }, - "type": "Note" - }, - "title": { - "en": [ - "Title of Achievement" - ] - }, - "type": "Qualification" - }, - "title": { - "en": [ - "TITLE OF PROGRAMME" - ] - }, - "type": "LearningAchievement" - } - ], - "id": "did:key:afsdlkj34134", - "identifier": [ - { - "id": "urn:epass:identifier:2", - "notation": "545465468", - "schemeName": "Student ID", - "type": "Identifier" - } - ], - "type": "Person" - }, - "description": [ - "EBSI Example https://github.com/Knowledge-Innovation-Centre/ESBI-JSON-schemas/blob/main/examples%20of%20credentials/DigiComp%20Generic.json" - ], - "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", - "issuancedate": "2024-03-26T16:06:50+01:00", - "issuer": "urn:epass:identifier:2", - "type": [ - "VerifiableCredential", - "VerifiableAttestation", - "EuropeanDigitalCredential" - ], - "validFrom": "2019-09-20T00:00:00+02:00" + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ] } \ No newline at end of file diff --git a/json/output_credential_ELM2OBv3.json b/json/output_credential_ELM2OBv3.json new file mode 100644 index 0000000..dd18456 --- /dev/null +++ b/json/output_credential_ELM2OBv3.json @@ -0,0 +1,574 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "awardedDate": "2024-03-26T16:06:50+01:00", + "awardeddate": "2019-09-20T00:00:00+02:00", + "credentialSchema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialschema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialsubject": { + "familyName": { + "en": [ + "Smith" + ] + }, + "fullName": { + "en": [ + "David Smith" + ] + }, + "givenName": { + "en": [ + "David" + ] + }, + "hasClaim": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "hasPart": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + }, + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "urn:epass:learningAchievement:1", + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievementSpecification" + }, + "title": { + "en": [ + "Topic #1" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "urn:epass:learningAchievement:2", + "provenBy": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "urn:epass:org:1", + "legalName": { + "en": [ + "University of Life" + ] + }, + "location": [ + { + "address": [ + { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "noteLiteral": { + "en": [ + "Here" + ] + }, + "type": "Note" + }, + "id": "urn:epass:address:1", + "type": "Address" + } + ], + "description": { + "en": [ + "The Address" + ] + }, + "id": "urn:epass:location:1", + "type": "Location" + } + ], + "registration": { + "id": "urn:epass:legalIdentifier:2", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": [ + "Belgium" + ] + }, + "type": "Concept" + }, + "type": "LegalIdentifier" + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "grade": { + "id": "urn:epass:note:2", + "noteLiteral": { + "en": [ + "10" + ] + }, + "type": "Note" + }, + "id": "urn:epass:learningAssessment:1", + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:1", + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessmentSpecification" + }, + "title": { + "en": [ + "Overall Diploma Assessment" + ] + }, + "type": "LearningAssessment" + } + ], + "specifiedBy": { + "eqfLevel": { + "id": "http://data.europa.eu/snb/eqf/5", + "inScheme": { + "id": "http://data.europa.eu/snb/eqf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "Level 5" + ] + }, + "type": "Concept" + }, + "id": "urn:epass:qualification:1", + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:2", + "relatedSkill": [ + { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + }, + { + "id": "urn:epass:learningOutcome:3", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/34v10n662m", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "3.1 Proficiency Level Foundation 2" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence 2" + ] + }, + "type": "LearningOutcome" + } + ], + "learningOutcomeSummary": { + "id": "urn:epass:note:3", + "noteLiteral": { + "en": [ + "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" + ] + }, + "type": "Note" + }, + "title": { + "en": [ + "Title of Achievement" + ] + }, + "type": "Qualification" + }, + "title": { + "en": [ + "TITLE OF PROGRAMME" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "did:key:afsdlkj34134", + "identifier": [ + { + "id": "urn:epass:identifier:2", + "notation": "545465468", + "schemeName": "Student ID", + "type": "Identifier" + } + ], + "type": "Person" + }, + "description": [ + "EBSI Example https://github.com/Knowledge-Innovation-Centre/ESBI-JSON-schemas/blob/main/examples%20of%20credentials/DigiComp%20Generic.json" + ], + "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", + "issuancedate": "2024-03-26T16:06:50+01:00", + "issuer": "urn:epass:identifier:2", + "type": [ + "VerifiableCredential", + "VerifiableAttestation", + "EuropeanDigitalCredential" + ], + "validFrom": "2019-09-20T00:00:00+02:00" +} \ No newline at end of file From 4e3b29002faadeb1df57e21da4d80701c39c33ec Mon Sep 17 00:00:00 2001 From: hamrt Date: Mon, 16 Sep 2024 09:52:46 +0200 Subject: [PATCH 03/45] added string into array function --- example_ELM-to-OBv3.json | 79 ++++++++++++++-- .../custom_mapping_ELM_OBv3_latest.json | 91 +++++++++++++++++-- src/backend/repository.rs | 51 +++++++++-- 3 files changed, 198 insertions(+), 23 deletions(-) diff --git a/example_ELM-to-OBv3.json b/example_ELM-to-OBv3.json index 1d04c66..287276e 100644 --- a/example_ELM-to-OBv3.json +++ b/example_ELM-to-OBv3.json @@ -1,12 +1,79 @@ { - "@context": "http://data.europa.eu/snb/model/context/edc-ap", + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "http://data.europa.eu/snb/model/context/edc-ap" + ], + "credentialSubject": { + "achievement": { + "criteria": { + "narrative": [ + { + "id": "urn:epass:learningOutcome:2", + "relatedSkill": [ + { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + }, + { + "id": "urn:epass:learningOutcome:3", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/34v10n662m", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "3.1 Proficiency Level Foundation 2" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence 2" + ] + }, + "type": "LearningOutcome" + } + ] + }, + "id": "urn:epass:learningAchievement:2", + "type": [ + "Achievement" + ] + }, + "id": "did:key:afsdlkj34134", + "type": [ + "AchievementSubject" + ] + }, "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", "issuer": { - "id": "urn:epass:org:1", - "name": "University of Life", - "type": { - "[0]": "Profile" - } + "id": "did:ebsi:org:12345689", + "name": "ORGANIZACION TEST", + "type": [ + "Profile" + ] }, "name": "TITLE OF PROGRAMME", "type": [ diff --git a/json/mapping/custom_mapping_ELM_OBv3_latest.json b/json/mapping/custom_mapping_ELM_OBv3_latest.json index 53c8ca9..cc3b4f6 100644 --- a/json/mapping/custom_mapping_ELM_OBv3_latest.json +++ b/json/mapping/custom_mapping_ELM_OBv3_latest.json @@ -36,7 +36,7 @@ "type_": "copy", "source": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].awardedBy.awardingBody[0].id" + "path": "$.issuer.id" }, "destination": { "format": "OBv3", @@ -50,14 +50,14 @@ }, "destination": { "format": "OBv3", - "path": "$.issuer.type.[0]" + "path": "$.issuer.type[]" } }, { "type_": "copy", "source": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].awardedBy.awardingBody[0].legalName.en[0]" + "path": "$.issuer.legalName.en" }, "destination": { "format": "OBv3", @@ -65,18 +65,57 @@ } }, { - "type_": { - "takeIndex": { - "index": 1 - } + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].id" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.id" + } + }, + { + "type_": "stringit", + "source": { + "value": "Achievement" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.type[]" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].id" }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.id" + } + }, + { + "type_": "copy", "source": { "format": "ELM", - "path": "$.@context" + "path": "$.credentialSubject.hasClaim[0].specifiedBy.title[0]" }, "destination": { "format": "OBv3", - "path": "$.@context" + "path": "$.credentialSubject.achievement.name" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.criteria.narrative" } }, { @@ -100,5 +139,39 @@ "format": "OBv3", "path": "$.name" } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.id" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.id" + } + }, + { + "type_": "stringit", + "source": { + "value": "AchievementSubject" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.type[]" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.id" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.id" + } } + + ] \ No newline at end of file diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 27945ab..95503da 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -140,20 +140,30 @@ impl Repository { }, } => { if destination_format != mapping.output_format() { - return; + return None; } - + let dest = destination_path.clone(); let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. let pointer = JsonPointer::try_from(JsonPath(destination_path)).unwrap(); let mut leaf_node = construct_leaf_node(&pointer); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(source_value); + if dest.contains("[]") { + // Deserialize the JSON string into a MyStruct + // remove the array element from the destantation and reuse this new pointer + // fill the array with the new value + let array_pointer = &pointer[..pointer.len()-2]; + if let Some(array) = leaf_node.pointer_mut(&array_pointer) .and_then(|v| v.as_array_mut()) { + // Push a new string to the array + array.push(Value::String(source_value)); + } + } + else if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(source_value); } merge(destination_credential, leaf_node); + None } @@ -195,13 +205,38 @@ pub fn construct_leaf_node(path: &str) -> Value { // Initialize the root of the JSON structure as null // todo: isn't this actually the value of the leaf node, not the root? let mut current_value = Value::Null; + // Iterate through the parts in reverse order to build the nested structure for part in parts.into_iter().rev() { - let mut new_object = Map::new(); - new_object.insert(part.to_string(), current_value); - current_value = Value::Object(new_object); + // handle arrays differnt + if part.contains("[]") { + let part_array: &str = &part[..part.len()-2]; + + let v: Vec = Vec::new(); + + // // ... fill in the vec with some Value::Object's as you like it ... + // // ... in our case its one or more strings + + let a = Value::Array(v); + // let mut map: serde_json::Map = serde_json::Map::new(); + // map.insert("person",a); + // let o = Value::Object(map); + + let mut new_object = Map::new(); + new_object.insert(part_array.to_string(),a); + current_value = Value::Object(new_object); + } + else { + let mut new_object = Map::new(); + new_object.insert(part.to_string(), current_value); + current_value = Value::Object(new_object); + + } } + // if the end of the path indicates an array than add an array to the leaf and return the array to be filled with a value + + current_value } From cd560f6fc996163894114e651796835c55ca4770 Mon Sep 17 00:00:00 2001 From: hamrt Date: Mon, 16 Sep 2024 10:41:10 +0200 Subject: [PATCH 04/45] update mappingfiles --- .../mapping/custom_mapping_ELM_OBv3_copy.json | 287 ------------------ .../custom_mapping_ELM_OBv3_latest.json | 12 +- 2 files changed, 11 insertions(+), 288 deletions(-) delete mode 100644 json/mapping/custom_mapping_ELM_OBv3_copy.json diff --git a/json/mapping/custom_mapping_ELM_OBv3_copy.json b/json/mapping/custom_mapping_ELM_OBv3_copy.json deleted file mode 100644 index 4d20058..0000000 --- a/json/mapping/custom_mapping_ELM_OBv3_copy.json +++ /dev/null @@ -1,287 +0,0 @@ -[ - { - "type_": "stringit", - "source": { - "value": "credentialSubjectje" - }, - "destination": { - "format": "OBv3", - "path": "$.name" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.evidence" - }, - "destination": { - "format": "OBv3", - "path": "$.evidence" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.issuanceDate" - }, - "destination": { - "format": "OBv3", - "path": "$.issuancedate" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.issuer" - }, - "destination": { - "format": "OBv3", - "path": "$.issuer" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.proof" - }, - "destination": { - "format": "OBv3", - "path": "$.proof" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.termsOfUse" - }, - "destination": { - "format": "OBv3", - "path": "$.termsofuse" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.validFrom" - }, - "destination": { - "format": "OBv3", - "path": "$.awardeddate" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.validUntil" - }, - "destination": { - "format": "OBv3", - "path": "$.expirationdate" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.refreshService" - }, - "destination": { - "format": "OBv3", - "path": "$.refreshservice" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.id" - }, - "destination": { - "format": "OBv3", - "path": "$.id" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.@context" - }, - "destination": { - "format": "OBv3", - "path": "$.@context" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.credentialSubject.id" - }, - "destination": { - "format": "OBv3", - "path": "$.credentialSubject.achievement.id" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.credentialSubject.hasClaim" - }, - "destination": { - "format": "OBv3", - "path": "$.credentialSubject.achievement.description" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.credentialSubject.fullName.en" - }, - "destination": { - "format": "OBv3", - "path": "$.credentialSubject.achievement.name" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.credentialSubject.type" - }, - "destination": { - "format": "OBv3", - "path": "$.credentialSubject.achievement.type" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.credentialSubject.identifier" - }, - "destination": { - "format": "OBv3", - "path": "$.credentialSubject.type" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.credentialSubject.hasClaim" - }, - "destination": { - "format": "OBv3", - "path": "$.credentialSubject.achievement.criteria" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.issuer.id" - }, - "destination": { - "format": "OBv3", - "path": "$.issuer" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.issuer.identifier.type" - }, - "destination": { - "format": "OBv3", - "path": "$.type" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.validFrom" - }, - "destination": { - "format": "OBv3", - "path": "$.validFrom" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.credentialSchema" - }, - "destination": { - "format": "OBv3", - "path": "$.credentialSchema" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.issuer.identifier.id" - }, - "destination": { - "format": "OBv3", - "path": "$.issuer" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.type" - }, - "destination": { - "format": "OBv3", - "path": "$.type" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.displayParameter.description.en" - }, - "destination": { - "format": "OBv3", - "path": "$.description" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.displayParameter.description.en" - }, - "destination": { - "format": "OBv3", - "path": "$.description" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.issuanceDate" - }, - "destination": { - "format": "OBv3", - "path": "$.awardedDate" - } - } -] \ No newline at end of file diff --git a/json/mapping/custom_mapping_ELM_OBv3_latest.json b/json/mapping/custom_mapping_ELM_OBv3_latest.json index cc3b4f6..fe8456d 100644 --- a/json/mapping/custom_mapping_ELM_OBv3_latest.json +++ b/json/mapping/custom_mapping_ELM_OBv3_latest.json @@ -171,7 +171,17 @@ "format": "OBv3", "path": "$.credentialSubject.id" } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.proof" + }, + "destination": { + "format": "OBv3", + "path": "$.proof" + } } - ] \ No newline at end of file From e7cbeb9fffe2a8e672984536629b5b49163600e5 Mon Sep 17 00:00:00 2001 From: hamrt Date: Mon, 16 Sep 2024 23:37:04 +0200 Subject: [PATCH 05/45] add markdown functionality --- example_ELM-to-OBv3.json | 51 +--- .../custom_mapping_ELM_OBv3_latest.json | 22 +- .../custom_mapping_ELM_OBv3_latest_copy.json | 187 ++++++++++++++ src/backend/repository.rs | 232 ++++++++++++++++++ src/backend/transformations.rs | 39 +++ 5 files changed, 470 insertions(+), 61 deletions(-) create mode 100644 json/mapping/custom_mapping_ELM_OBv3_latest_copy.json diff --git a/example_ELM-to-OBv3.json b/example_ELM-to-OBv3.json index 287276e..2da6178 100644 --- a/example_ELM-to-OBv3.json +++ b/example_ELM-to-OBv3.json @@ -6,56 +6,7 @@ "credentialSubject": { "achievement": { "criteria": { - "narrative": [ - { - "id": "urn:epass:learningOutcome:2", - "relatedSkill": [ - { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - }, - { - "id": "urn:epass:learningOutcome:3", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/34v10n662m", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "3.1 Proficiency Level Foundation 2" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence 2" - ] - }, - "type": "LearningOutcome" - } - ] + "narrative": "- **id**:\n urn:epass:learningOutcome:2\n\n **relatedSkill**:\n - **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo\n\n **inScheme**:\n **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/25831c2\n\n **type**:\n ConceptScheme\n\n\n **prefLabel**:\n **en**:\n - 5.4 Identifying digital competence gaps\n\n\n **type**:\n Concept\n\n **title**:\n **en**:\n - Name of DigiComp Competence\n\n\n **type**:\n LearningOutcome\n- **id**:\n urn:epass:learningOutcome:3\n\n **relatedSkill**:\n - **id**:\n http://data.europa.eu/snb/dcf/34v10n662m\n\n **inScheme**:\n **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/25831c2\n\n **type**:\n ConceptScheme\n\n\n **prefLabel**:\n **en**:\n - 3.1 Proficiency Level Foundation 2\n\n\n **type**:\n Concept\n\n **title**:\n **en**:\n - Name of DigiComp Competence 2\n\n\n **type**:\n LearningOutcome\n" }, "id": "urn:epass:learningAchievement:2", "type": [ diff --git a/json/mapping/custom_mapping_ELM_OBv3_latest.json b/json/mapping/custom_mapping_ELM_OBv3_latest.json index fe8456d..2e997d6 100644 --- a/json/mapping/custom_mapping_ELM_OBv3_latest.json +++ b/json/mapping/custom_mapping_ELM_OBv3_latest.json @@ -107,17 +107,6 @@ "path": "$.credentialSubject.achievement.name" } }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" - }, - "destination": { - "format": "OBv3", - "path": "$.credentialSubject.achievement.criteria.narrative" - } - }, { "type_": "copy", "source": { @@ -182,6 +171,17 @@ "format": "OBv3", "path": "$.proof" } + }, + { + "type_": "markdownit", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.criteria.narrative" + } } ] \ No newline at end of file diff --git a/json/mapping/custom_mapping_ELM_OBv3_latest_copy.json b/json/mapping/custom_mapping_ELM_OBv3_latest_copy.json new file mode 100644 index 0000000..fe8456d --- /dev/null +++ b/json/mapping/custom_mapping_ELM_OBv3_latest_copy.json @@ -0,0 +1,187 @@ +[ + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.@context" + }, + "destination": { + "format": "OBv3", + "path": "$.@context" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.id" + }, + "destination": { + "format": "OBv3", + "path": "$.id" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.type" + }, + "destination": { + "format": "OBv3", + "path": "$.type" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuer.id" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer.id" + } + }, + { + "type_": "stringit", + "source": { + "value": "Profile" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer.type[]" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuer.legalName.en" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer.name" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].id" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.id" + } + }, + { + "type_": "stringit", + "source": { + "value": "Achievement" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.type[]" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].id" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.id" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.title[0]" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.name" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.criteria.narrative" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.validFrom" + }, + "destination": { + "format": "OBv3", + "path": "$.validFrom" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].title.en[0]" + }, + "destination": { + "format": "OBv3", + "path": "$.name" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.id" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.id" + } + }, + { + "type_": "stringit", + "source": { + "value": "AchievementSubject" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.type[]" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSubject.id" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.id" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.proof" + }, + "destination": { + "format": "OBv3", + "path": "$.proof" + } + } + +] \ No newline at end of file diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 95503da..6f9cb90 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -12,6 +12,7 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, }; +use regex::Regex; #[derive(Debug, Default, Clone)] pub struct Repository(HashMap); @@ -166,7 +167,106 @@ impl Repository { None } + Transformation::JsonToMarkdown { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + + // run the source value through a markdown converter to fit the nested objects into a markdown string + let markdown_source_value = json!(json_to_markdown(&source_value, 0)); + + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(markdown_source_value); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + Transformation::MarkdownToJson { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + + // run the source value through a markdown converter to fit the nested objects into a markdown string + let json_source_value = json!(markdown_to_json(&source_value.to_string())); + + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(json_source_value); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + + _ => todo!(), } } @@ -289,3 +389,135 @@ fn remove_key_recursive(current_json: &mut Value, keys: &[String]) -> bool { false } + +fn json_to_markdown(json: &Value, indent_level: usize) -> String { + let mut markdown = String::new(); + let indent = " ".repeat(indent_level); + + match json { + Value::Object(map) => { + for (key, value) in map { + markdown.push_str(&format!("{}**{}**:\n", indent, key)); + markdown.push_str(&json_to_markdown(value, indent_level + 1)); + markdown.push('\n'); + } + } + Value::Array(arr) => { + for item in arr { + markdown.push_str(&format!("{}- {}\n", indent, json_to_markdown(item, indent_level + 1).trim())); + } + } + Value::String(s) => { + markdown.push_str(&format!("{}{}\n", indent, s)); + } + Value::Number(n) => { + markdown.push_str(&format!("{}{}\n", indent, n)); + } + Value::Bool(b) => { + markdown.push_str(&format!("{}{}\n", indent, b)); + } + Value::Null => { + markdown.push_str(&format!("{}null\n", indent)); + } + } + + markdown +} + + + +fn markdown_to_json(markdown: &str) -> Value { + let mut lines = markdown.lines().peekable(); + let mut current_indent = 0; + let mut stack: Vec = vec![Value::Object(Default::default())]; + let mut current_key: Option = None; + + let heading_regex = Regex::new(r"^#+ (.+)").unwrap(); + let bold_regex = Regex::new(r"^\s*\*\*(.+?)\*\*\s*:$").unwrap(); + let list_item_regex = Regex::new(r"^\s*-\s*(.+)").unwrap(); + + while let Some(line) = lines.next() { + let line_indent = line.chars().take_while(|c| c.is_whitespace()).count(); + let line = line.trim(); + + if line.is_empty() { + continue; + } + + // Adjust stack based on indentation + if line_indent > current_indent { + stack.push(Value::Object(Default::default())); + } else if line_indent < current_indent { + let value = stack.pop().unwrap(); + let parent = stack.last_mut().unwrap(); + if let Some(key) = current_key.take() { + if let Value::Object(ref mut obj) = parent { + obj.insert(key, value); + } + } else if let Value::Array(ref mut arr) = parent { + arr.push(value); + } + } + + current_indent = line_indent; + + if let Some(caps) = heading_regex.captures(line) { + let heading = caps.get(1).unwrap().as_str().trim().to_string(); + current_key = Some(heading); + } else if let Some(caps) = bold_regex.captures(line) { + let key = caps.get(1).unwrap().as_str().trim().to_string(); + let parent = stack.last_mut().unwrap(); + if let Value::Object(ref mut obj) = parent { + obj.insert(key.clone(), Value::Null); + } + current_key = Some(key); + } else if let Some(caps) = list_item_regex.captures(line) { + let item = caps.get(1).unwrap().as_str().trim().to_string(); + let parent = stack.last_mut().unwrap(); + if let Value::Array(ref mut arr) = parent { + arr.push(Value::String(item)); + } else { + let arr = vec![Value::String(item)]; + stack.push(Value::Array(arr)); + } + } else { + let parent = stack.last_mut().unwrap(); + if let Some(key) = current_key.take() { + if let Value::Object(ref mut obj) = parent { + obj.insert(key, Value::String(line.to_string())); + } + } else if let Value::Array(ref mut arr) = parent { + arr.push(Value::String(line.to_string())); + } + } + } + + // Handle remaining items in the stack + while stack.len() > 1 { + let value = stack.pop().unwrap(); + let parent = stack.last_mut().unwrap(); + if let Some(key) = current_key.take() { + if let Value::Object(ref mut obj) = parent { + obj.insert(key, value); + } + } else if let Value::Array(ref mut arr) = parent { + arr.push(value); + } + } + + stack.pop().unwrap() +} + +// 1. Parsing Markdown: +// • Headings (#): These are treated as keys in the resulting JSON object. +// • Bold Text (**): This is also treated as a key in the JSON object. +// • List Items (-): These are treated as elements in a JSON array. +// • Plain Text: If it’s not part of a list or a key, it’s treated as a value associated with the last key in the current JSON object. +// 2. Indentation Handling: +// • The code tracks the current indentation level of the Markdown. If the indentation increases, it means a new nested structure (object or array) is starting. If it decreases, the last completed structure is attached to the parent object or array. +// 3. Stack Management: +// • A stack is used to manage the nested structure. Each time a new nested object or array is detected, it’s pushed onto the stack. Once the nesting ends (indentation decreases), the structure is popped from the stack and integrated into the parent structure. +// 4. Regex Patterns: +// • heading_regex: Matches Markdown headings (e.g., # Title). +// • bold_regex: Matches bolded keys (e.g., **Key**:). +// • list_item_regex: Matches list items (e.g., - item). \ No newline at end of file diff --git a/src/backend/transformations.rs b/src/backend/transformations.rs index 906a1bf..b499b1a 100644 --- a/src/backend/transformations.rs +++ b/src/backend/transformations.rs @@ -110,6 +110,35 @@ impl StringToOne { } +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum JsonToMarkdown { + jsonToMarkdown, +} + +impl JsonToMarkdown { + pub fn apply(&self, value: Value) -> Value { + match self { + JsonToMarkdown::jsonToMarkdown => value, + } + } +} + +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum MarkdownToJson { + markdownToJson, +} + +impl MarkdownToJson { + pub fn apply(&self, value: Value) -> Value { + match self { + MarkdownToJson::markdownToJson => value, + } + } +} + + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] @@ -124,6 +153,16 @@ pub enum Transformation { source: StringValue, destination: DataLocation, }, + MarkdownToJson { + type_: MarkdownToJson, + source: DataLocation, + destination: DataLocation, + }, + JsonToMarkdown { + type_: JsonToMarkdown, + source: DataLocation, + destination: DataLocation, + }, OneToMany { type_: OneToMany, source: DataLocation, From 7825865f5a717a864e59db8bd65a599fef0fddc6 Mon Sep 17 00:00:00 2001 From: hamrt Date: Mon, 16 Sep 2024 23:37:57 +0200 Subject: [PATCH 06/45] update example mapping --- json/mapping/custom_mapping_ELM_OBv3_latest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/json/mapping/custom_mapping_ELM_OBv3_latest.json b/json/mapping/custom_mapping_ELM_OBv3_latest.json index 2e997d6..ffc2fb1 100644 --- a/json/mapping/custom_mapping_ELM_OBv3_latest.json +++ b/json/mapping/custom_mapping_ELM_OBv3_latest.json @@ -173,7 +173,7 @@ } }, { - "type_": "markdownit", + "type_": "jsonToMarkdown", "source": { "format": "ELM", "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" From 347b57f858914f9886b7e58741bd3eb1d30e6d8d Mon Sep 17 00:00:00 2001 From: hamrt Date: Mon, 16 Sep 2024 23:48:37 +0200 Subject: [PATCH 07/45] improved mapping --- example_ELM-to-OBv3.json | 1 + json/mapping/custom_mapping_ELM_OBv3_latest.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/example_ELM-to-OBv3.json b/example_ELM-to-OBv3.json index 2da6178..96467cd 100644 --- a/example_ELM-to-OBv3.json +++ b/example_ELM-to-OBv3.json @@ -9,6 +9,7 @@ "narrative": "- **id**:\n urn:epass:learningOutcome:2\n\n **relatedSkill**:\n - **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo\n\n **inScheme**:\n **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/25831c2\n\n **type**:\n ConceptScheme\n\n\n **prefLabel**:\n **en**:\n - 5.4 Identifying digital competence gaps\n\n\n **type**:\n Concept\n\n **title**:\n **en**:\n - Name of DigiComp Competence\n\n\n **type**:\n LearningOutcome\n- **id**:\n urn:epass:learningOutcome:3\n\n **relatedSkill**:\n - **id**:\n http://data.europa.eu/snb/dcf/34v10n662m\n\n **inScheme**:\n **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/25831c2\n\n **type**:\n ConceptScheme\n\n\n **prefLabel**:\n **en**:\n - 3.1 Proficiency Level Foundation 2\n\n\n **type**:\n Concept\n\n **title**:\n **en**:\n - Name of DigiComp Competence 2\n\n\n **type**:\n LearningOutcome\n" }, "id": "urn:epass:learningAchievement:2", + "name": "Title of Achievement", "type": [ "Achievement" ] diff --git a/json/mapping/custom_mapping_ELM_OBv3_latest.json b/json/mapping/custom_mapping_ELM_OBv3_latest.json index ffc2fb1..91e1ddf 100644 --- a/json/mapping/custom_mapping_ELM_OBv3_latest.json +++ b/json/mapping/custom_mapping_ELM_OBv3_latest.json @@ -100,7 +100,7 @@ "type_": "copy", "source": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].specifiedBy.title[0]" + "path": "$.credentialSubject.hasClaim[0].specifiedBy.title.*.[0]" }, "destination": { "format": "OBv3", From 74e07406a5852fd9c680a4decb2de8eeccf67964 Mon Sep 17 00:00:00 2001 From: hamrt Date: Wed, 18 Sep 2024 10:29:28 +0200 Subject: [PATCH 08/45] initial back and forth test --- example_ELM-to-OBv3.json | 10 + example_OBv3-to-ELM.json | 1062 ----------------- example_OBv3_to_ELM.json | 46 + .../custom_mapping_ELM_OBv3_latest.json | 11 + .../custom_mapping_OBv3_ELM_latest copy.json | 135 +++ ...on => custom_mapping_OBv3_ELM_latest.json} | 130 +- json/output_credential.json | 39 +- json/output_credential_ELM2OBv3.json | 574 --------- 8 files changed, 287 insertions(+), 1720 deletions(-) delete mode 100644 example_OBv3-to-ELM.json create mode 100644 example_OBv3_to_ELM.json create mode 100644 json/mapping/custom_mapping_OBv3_ELM_latest copy.json rename json/mapping/{custom_mapping.json => custom_mapping_OBv3_ELM_latest.json} (62%) delete mode 100644 json/output_credential_ELM2OBv3.json diff --git a/example_ELM-to-OBv3.json b/example_ELM-to-OBv3.json index 96467cd..9f09aa9 100644 --- a/example_ELM-to-OBv3.json +++ b/example_ELM-to-OBv3.json @@ -3,6 +3,16 @@ "https://www.w3.org/ns/credentials/v2", "http://data.europa.eu/snb/model/context/edc-ap" ], + "credentialSchema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], "credentialSubject": { "achievement": { "criteria": { diff --git a/example_OBv3-to-ELM.json b/example_OBv3-to-ELM.json deleted file mode 100644 index 0fecb4f..0000000 --- a/example_OBv3-to-ELM.json +++ /dev/null @@ -1,1062 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "http://data.europa.eu/snb/model/context/edc-ap" - ], - "awardedDate": "2024-03-26T16:06:50+01:00", - "awardeddate": "2019-09-20T00:00:00+02:00", - "credentialSchema": [ - { - "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", - "type": "ShaclValidator2017" - }, - { - "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", - "type": "JsonSchema" - } - ], - "credentialSubject": { - "achievement": { - "criteria": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "hasPart": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - }, - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - } - ], - "id": "urn:epass:learningAchievement:2", - "provenBy": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "grade": { - "id": "urn:epass:note:2", - "noteLiteral": { - "en": [ - "10" - ] - }, - "type": "Note" - }, - "id": "urn:epass:learningAssessment:1", - "specifiedBy": { - "id": "urn:epass:learningAssessmentSpec:1", - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessmentSpecification" - }, - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessment" - } - ], - "specifiedBy": { - "eqfLevel": { - "id": "http://data.europa.eu/snb/eqf/5", - "inScheme": { - "id": "http://data.europa.eu/snb/eqf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "Level 5" - ] - }, - "type": "Concept" - }, - "id": "urn:epass:qualification:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:2", - "relatedSkill": [ - { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - }, - { - "id": "urn:epass:learningOutcome:3", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/34v10n662m", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "3.1 Proficiency Level Foundation 2" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence 2" - ] - }, - "type": "LearningOutcome" - } - ], - "learningOutcomeSummary": { - "id": "urn:epass:note:3", - "noteLiteral": { - "en": [ - "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" - ] - }, - "type": "Note" - }, - "title": { - "en": [ - "Title of Achievement" - ] - }, - "type": "Qualification" - }, - "title": { - "en": [ - "TITLE OF PROGRAMME" - ] - }, - "type": "LearningAchievement" - } - ], - "description": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "hasPart": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - }, - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - } - ], - "id": "urn:epass:learningAchievement:2", - "provenBy": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "grade": { - "id": "urn:epass:note:2", - "noteLiteral": { - "en": [ - "10" - ] - }, - "type": "Note" - }, - "id": "urn:epass:learningAssessment:1", - "specifiedBy": { - "id": "urn:epass:learningAssessmentSpec:1", - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessmentSpecification" - }, - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessment" - } - ], - "specifiedBy": { - "eqfLevel": { - "id": "http://data.europa.eu/snb/eqf/5", - "inScheme": { - "id": "http://data.europa.eu/snb/eqf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "Level 5" - ] - }, - "type": "Concept" - }, - "id": "urn:epass:qualification:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:2", - "relatedSkill": [ - { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - }, - { - "id": "urn:epass:learningOutcome:3", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/34v10n662m", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "3.1 Proficiency Level Foundation 2" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence 2" - ] - }, - "type": "LearningOutcome" - } - ], - "learningOutcomeSummary": { - "id": "urn:epass:note:3", - "noteLiteral": { - "en": [ - "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" - ] - }, - "type": "Note" - }, - "title": { - "en": [ - "Title of Achievement" - ] - }, - "type": "Qualification" - }, - "title": { - "en": [ - "TITLE OF PROGRAMME" - ] - }, - "type": "LearningAchievement" - } - ], - "id": "did:key:afsdlkj34134", - "name": [ - "David Smith" - ], - "type": "Person" - }, - "type": [ - { - "id": "urn:epass:identifier:2", - "notation": "545465468", - "schemeName": "Student ID", - "type": "Identifier" - } - ] - }, - "credentialsubject": "credentialSubjectje", - "description": [ - "EBSI Example https://github.com/Knowledge-Innovation-Centre/ESBI-JSON-schemas/blob/main/examples%20of%20credentials/DigiComp%20Generic.json" - ], - "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", - "issuancedate": "2024-03-26T16:06:50+01:00", - "issuer": "urn:epass:identifier:2", - "type": [ - "VerifiableCredential", - "VerifiableAttestation", - "EuropeanDigitalCredential" - ], - "validFrom": "2019-09-20T00:00:00+02:00" -} \ No newline at end of file diff --git a/example_OBv3_to_ELM.json b/example_OBv3_to_ELM.json new file mode 100644 index 0000000..6815bf7 --- /dev/null +++ b/example_OBv3_to_ELM.json @@ -0,0 +1,46 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "http://data.europa.eu/snb/model/context/edc-ap" + ], + "credentialSchema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialSubject": { + "hasClaim[0]": { + "id": "urn:epass:learningAchievement:2", + "specifiedBy": { + "learningOutcome": {}, + "title": { + "en": { + "[0]": "Title of Achievement" + } + } + }, + "title": { + "en[0]": "TITLE OF PROGRAMME" + } + }, + "id": "did:key:afsdlkj34134" + }, + "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", + "issuer": { + "id": "did:ebsi:org:12345689", + "legalName": { + "en": "ORGANIZACION TEST" + } + }, + "type": [ + "VerifiableCredential", + "VerifiableAttestation", + "EuropeanDigitalCredential" + ], + "validFrom": "2019-09-20T00:00:00+02:00" +} \ No newline at end of file diff --git a/json/mapping/custom_mapping_ELM_OBv3_latest.json b/json/mapping/custom_mapping_ELM_OBv3_latest.json index 91e1ddf..3534b17 100644 --- a/json/mapping/custom_mapping_ELM_OBv3_latest.json +++ b/json/mapping/custom_mapping_ELM_OBv3_latest.json @@ -182,6 +182,17 @@ "format": "OBv3", "path": "$.credentialSubject.achievement.criteria.narrative" } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialSchema" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSchema" + } } ] \ No newline at end of file diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest copy.json b/json/mapping/custom_mapping_OBv3_ELM_latest copy.json new file mode 100644 index 0000000..a2b2b85 --- /dev/null +++ b/json/mapping/custom_mapping_OBv3_ELM_latest copy.json @@ -0,0 +1,135 @@ +[ + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.@context" + }, + "destination": { + "format": "ELM", + "path": "$.@context" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.id" + }, + "destination": { + "format": "ELM", + "path": "$.id" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.type" + }, + "destination": { + "format": "ELM", + "path": "$.type" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.issuer.id" + }, + "destination": { + "format": "ELM", + "path": "$.issuer.id" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.issuer.name" + }, + "destination": { + "format": "ELM", + "path": "$.issuer.legalName.en" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.id" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].id" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.name" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.title.en.[0]" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.validFrom" + }, + "destination": { + "format": "ELM", + "path": "$.validFrom" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.name" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].title.en[0]" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.id" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.id" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.proof" + }, + "destination": { + "format": "ELM", + "path": "$.proof" + } + }, + { + "type_": "markdownToJson", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.criteria.narrative" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" + } + } + +] \ No newline at end of file diff --git a/json/mapping/custom_mapping.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json similarity index 62% rename from json/mapping/custom_mapping.json rename to json/mapping/custom_mapping_OBv3_ELM_latest.json index 7bac0ef..4c6a1c5 100644 --- a/json/mapping/custom_mapping.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -2,177 +2,145 @@ { "type_": "copy", "source": { - "format": "ELM", + "format": "OBv3", "path": "$.@context" }, "destination": { - "format": "OBv3", + "format": "ELM", "path": "$.@context" } }, { "type_": "copy", "source": { - "format": "ELM", - "path": "$.credentialSubject.familyName.en" + "format": "OBv3", + "path": "$.id" }, "destination": { - "format": "OBv3", - "path": "$.credentialSubject.type.items" + "format": "ELM", + "path": "$.id" } }, { "type_": "copy", "source": { - "format": "ELM", + "format": "OBv3", "path": "$.type" }, "destination": { - "format": "OBv3", - "path": "$.credentialSubject.type.contains" - } - }, - { - "type_": "copy", - "source": { "format": "ELM", - "path": "$.credentialSchema" - }, - "destination": { - "format": "OBv3", - "path": "$.credentialschema" + "path": "$.type" } }, { "type_": "copy", "source": { - "format": "ELM", - "path": "$.credentialStatus" - }, - "destination": { "format": "OBv3", - "path": "$.credentialstatus" - } - }, - { - "type_": "copy", - "source": { - "format": "ELM", - "path": "$.credentialSubject" + "path": "$.issuer.id" }, "destination": { - "format": "OBv3", - "path": "$.credentialsubject" - } - }, - { - "type_": "copy", - "source": { "format": "ELM", - "path": "$.evidence" - }, - "destination": { - "format": "OBv3", - "path": "$.evidence" + "path": "$.issuer.id" } }, { "type_": "copy", "source": { - "format": "ELM", - "path": "$.issuanceDate" + "format": "OBv3", + "path": "$.issuer.name" }, "destination": { - "format": "OBv3", - "path": "$.issuancedate" + "format": "ELM", + "path": "$.issuer.legalName.en" } }, { "type_": "copy", "source": { - "format": "ELM", - "path": "$.issuer" + "format": "OBv3", + "path": "$.credentialSubject.achievement.id" }, "destination": { - "format": "OBv3", - "path": "$.issuer" + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].id" } }, { "type_": "copy", "source": { - "format": "ELM", - "path": "$.proof" - }, - "destination": { "format": "OBv3", - "path": "$.proof" + "path": "$.credentialSubject.achievement.name" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.title.en.[0]" } }, { "type_": "copy", "source": { - "format": "ELM", - "path": "$.termsOfUse" + "format": "OBv3", + "path": "$.validFrom" }, "destination": { - "format": "OBv3", - "path": "$.termsofuse" + "format": "ELM", + "path": "$.validFrom" } }, { "type_": "copy", "source": { - "format": "ELM", - "path": "$.validFrom" + "format": "OBv3", + "path": "$.name" }, "destination": { - "format": "OBv3", - "path": "$.awardeddate" + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].title.en[0]" } }, { "type_": "copy", "source": { - "format": "ELM", - "path": "$.validUntil" + "format": "OBv3", + "path": "$.credentialSubject.id" }, "destination": { - "format": "OBv3", - "path": "$.expirationdate" + "format": "ELM", + "path": "$.credentialSubject.id" } }, { "type_": "copy", "source": { - "format": "ELM", - "path": "$.refreshService" + "format": "OBv3", + "path": "$.proof" }, "destination": { - "format": "OBv3", - "path": "$.refreshservice" + "format": "ELM", + "path": "$.proof" } }, { "type_": "copy", "source": { - "format": "ELM", - "path": "$.validFrom" + "format": "OBv3", + "path": "$.credentialSchema" }, "destination": { - "format": "OBv3", - "path": "$.validFrom" + "format": "ELM", + "path": "$.credentialSchema" } }, { - "type_": "copy", + "type_": "markdownToJson", "source": { - "format": "ELM", - "path": "$.id" + "format": "OBv3", + "path": "$.credentialSubject.achievement.criteria.narrative" }, "destination": { - "format": "OBv3", - "path": "$.id" + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" } } + ] \ No newline at end of file diff --git a/json/output_credential.json b/json/output_credential.json index 9169f44..022d047 100644 --- a/json/output_credential.json +++ b/json/output_credential.json @@ -1,6 +1,39 @@ { "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" - ] + "https://www.w3.org/ns/credentials/v2" + ], + "Issuer": { + "id": "https://example.com/issuers/876543", + "name": "Example Corp", + "type": [ + "Profile" + ] + }, + "Proof": [ + { + "created": "2024-05-31T14:05:25Z", + "cryptosuite": "eddsa-rdfc-2022", + "proofPurpose": "assertionMethod", + "proofValue": "zJPcuWH556eQMEiPVd1Emp85PNTgsRyVYbMcxjsQXJ4MBQ2yEn83CQyJDjpvUSNx8GTSpiCC5pdYBou5gyTvnSwx", + "type": "DataIntegrityProof", + "verificationMethod": "https://example.com/issuers/876543#z6MksaRBVxaAjk4dNYcw5tReeHK6VAVVpjTyAzb4NofmhoGi" + } + ], + "credentialSubject": { + "achievement": { + "criteria": { + "narrative": "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + }, + "description": "This badge recognizes the development of the capacity to collaborate within a group environment.", + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "name": "Teamwork", + "type": [ + "Achievement" + ] + }, + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ] + } } \ No newline at end of file diff --git a/json/output_credential_ELM2OBv3.json b/json/output_credential_ELM2OBv3.json deleted file mode 100644 index dd18456..0000000 --- a/json/output_credential_ELM2OBv3.json +++ /dev/null @@ -1,574 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" - ], - "awardedDate": "2024-03-26T16:06:50+01:00", - "awardeddate": "2019-09-20T00:00:00+02:00", - "credentialSchema": [ - { - "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", - "type": "ShaclValidator2017" - }, - { - "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", - "type": "JsonSchema" - } - ], - "credentialschema": [ - { - "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", - "type": "ShaclValidator2017" - }, - { - "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", - "type": "JsonSchema" - } - ], - "credentialsubject": { - "familyName": { - "en": [ - "Smith" - ] - }, - "fullName": { - "en": [ - "David Smith" - ] - }, - "givenName": { - "en": [ - "David" - ] - }, - "hasClaim": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "hasPart": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - }, - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "id": "urn:epass:learningAchievement:1", - "specifiedBy": { - "id": "urn:epass:learningAchievementSpec:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:1", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievementSpecification" - }, - "title": { - "en": [ - "Topic #1" - ] - }, - "type": "LearningAchievement" - } - ], - "id": "urn:epass:learningAchievement:2", - "provenBy": [ - { - "awardedBy": { - "awardingBody": [ - { - "id": "urn:epass:org:1", - "legalName": { - "en": [ - "University of Life" - ] - }, - "location": [ - { - "address": [ - { - "countryCode": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "fullAddress": { - "id": "urn:epass:note:1", - "noteLiteral": { - "en": [ - "Here" - ] - }, - "type": "Note" - }, - "id": "urn:epass:address:1", - "type": "Address" - } - ], - "description": { - "en": [ - "The Address" - ] - }, - "id": "urn:epass:location:1", - "type": "Location" - } - ], - "registration": { - "id": "urn:epass:legalIdentifier:2", - "notation": "987654321", - "spatial": { - "id": "http://publications.europa.eu/resource/authority/country/BEL", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/country", - "type": "ConceptScheme" - }, - "notation": "country", - "prefLabel": { - "en": [ - "Belgium" - ] - }, - "type": "Concept" - }, - "type": "LegalIdentifier" - }, - "type": "Organisation" - } - ], - "id": "urn:epass:awardingProcess:1", - "type": "AwardingProcess" - }, - "grade": { - "id": "urn:epass:note:2", - "noteLiteral": { - "en": [ - "10" - ] - }, - "type": "Note" - }, - "id": "urn:epass:learningAssessment:1", - "specifiedBy": { - "id": "urn:epass:learningAssessmentSpec:1", - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessmentSpecification" - }, - "title": { - "en": [ - "Overall Diploma Assessment" - ] - }, - "type": "LearningAssessment" - } - ], - "specifiedBy": { - "eqfLevel": { - "id": "http://data.europa.eu/snb/eqf/5", - "inScheme": { - "id": "http://data.europa.eu/snb/eqf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "Level 5" - ] - }, - "type": "Concept" - }, - "id": "urn:epass:qualification:1", - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:2", - "relatedSkill": [ - { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - }, - { - "id": "urn:epass:learningOutcome:3", - "relatedSkill": [ - { - "id": "http://data.europa.eu/snb/dcf/34v10n662m", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "3.1 Proficiency Level Foundation 2" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence 2" - ] - }, - "type": "LearningOutcome" - } - ], - "learningOutcomeSummary": { - "id": "urn:epass:note:3", - "noteLiteral": { - "en": [ - "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" - ] - }, - "type": "Note" - }, - "title": { - "en": [ - "Title of Achievement" - ] - }, - "type": "Qualification" - }, - "title": { - "en": [ - "TITLE OF PROGRAMME" - ] - }, - "type": "LearningAchievement" - } - ], - "id": "did:key:afsdlkj34134", - "identifier": [ - { - "id": "urn:epass:identifier:2", - "notation": "545465468", - "schemeName": "Student ID", - "type": "Identifier" - } - ], - "type": "Person" - }, - "description": [ - "EBSI Example https://github.com/Knowledge-Innovation-Centre/ESBI-JSON-schemas/blob/main/examples%20of%20credentials/DigiComp%20Generic.json" - ], - "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", - "issuancedate": "2024-03-26T16:06:50+01:00", - "issuer": "urn:epass:identifier:2", - "type": [ - "VerifiableCredential", - "VerifiableAttestation", - "EuropeanDigitalCredential" - ], - "validFrom": "2019-09-20T00:00:00+02:00" -} \ No newline at end of file From bf0cdcd8eb525efaaee81e245aa709b7051bdb80 Mon Sep 17 00:00:00 2001 From: Oran Dan Date: Mon, 16 Sep 2024 11:00:44 +0200 Subject: [PATCH 09/45] chore: remove .vscode folder --- .vscode/launch.json | 16 ---------------- .vscode/settings.json | 5 ----- 2 files changed, 21 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 10efcb2..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "lldb", - "request": "launch", - "name": "Debug", - "program": "${workspaceFolder}/", - "args": [], - "cwd": "${workspaceFolder}" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index b242572..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "githubPullRequests.ignoredPullRequestBranches": [ - "main" - ] -} \ No newline at end of file From 88bbf02b5dacdd50f0bf38c6f41573a4e696dbd2 Mon Sep 17 00:00:00 2001 From: Oran Dan Date: Wed, 18 Sep 2024 11:13:57 +0200 Subject: [PATCH 10/45] chore: update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8c7ef3..8e8c633 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ On the bottom you'll find a bar explaining the basic keys as well. `res/output_credential.json`: Example output file for the converted JSON. `res/custom_mapping.json`: Example custom mapping file. -Logs are kept in `logging_folder/credential-converter.log`. This file is overwritten upon each startup of the program. +Logs are kept in `logging_folder/credential-converter.log`. This file is overwritten upon each startup of the program. Use the macro `trace_dbg!()` to add debug messages in the code. To remove the default file paths remove lines 34 - 38 from the `main.rs`: ```sh From f73ecd0e8c14ecc6dc3083d9394c67957744d663 Mon Sep 17 00:00:00 2001 From: Oran Dan Date: Wed, 18 Sep 2024 13:49:53 +0200 Subject: [PATCH 11/45] chore: move construct_leaf_node to leaf_node.rs --- src/backend/leaf_nodes.rs | 40 ++++++++++++++++++++++++++++++++- src/backend/repository.rs | 47 ++------------------------------------- 2 files changed, 41 insertions(+), 46 deletions(-) diff --git a/src/backend/leaf_nodes.rs b/src/backend/leaf_nodes.rs index e9e38dd..e9a0d40 100644 --- a/src/backend/leaf_nodes.rs +++ b/src/backend/leaf_nodes.rs @@ -1,4 +1,4 @@ -use serde_json::Value; +use serde_json::{Map, Value}; use std::collections::HashMap; pub fn extract_leaf_nodes(json_object: &Value, path: String, result: &mut HashMap) { @@ -27,3 +27,41 @@ pub fn get_leaf_nodes(json_object: Value) -> HashMap { .map(|(key, value)| (format!("/{}", key), value)) .collect() } + +pub fn construct_leaf_node(path: &str) -> Value { + // Split the input string by '/' and filter out any empty parts + let parts: Vec<&str> = path.split('/').filter(|&s| !s.is_empty()).collect(); + + // Initialize the root of the JSON structure as null + let mut current_value = Value::Null; + + + // Iterate through the parts in reverse order to build the nested structure + for part in parts.into_iter().rev() { + // handle arrays differnt + if part.contains("[]") { // todo: this should include the number ofcourse + let part_array: &str = &part[..part.len()-2]; + + let v: Vec = Vec::new(); + + // // ... fill in the vec with some Value::Object's as you like it ... + // // ... in our case its one or more strings + + let a = Value::Array(v); + + let mut new_object = Map::new(); + new_object.insert(part_array.to_string(),a); + current_value = Value::Object(new_object); + } + else { + let mut new_object = Map::new(); + new_object.insert(part.to_string(), current_value); + current_value = Value::Object(new_object); + + } + } + + // if the end of the path indicates an array than add an array to the leaf and return the array to be filled with a value + + current_value +} diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 6f9cb90..2765ace 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -1,13 +1,12 @@ use crate::{ backend::{ - jsonpointer::{JsonPath, JsonPointer}, - transformations::{DataLocation, StringValue, Transformation}, + jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, transformations::{DataLocation, StringValue, Transformation} }, state::{AppState, Mapping}, trace_dbg, }; use jsonpath_rust::JsonPathFinder; -use serde_json::{json, Map, Value}; +use serde_json::{json, Value}; use std::{ collections::HashMap, ops::{Deref, DerefMut}, @@ -298,48 +297,6 @@ impl Repository { } } -pub fn construct_leaf_node(path: &str) -> Value { - // Split the input string by '/' and filter out any empty parts - let parts: Vec<&str> = path.split('/').filter(|&s| !s.is_empty()).collect(); - - // Initialize the root of the JSON structure as null // todo: isn't this actually the value of the leaf node, not the root? - let mut current_value = Value::Null; - - - // Iterate through the parts in reverse order to build the nested structure - for part in parts.into_iter().rev() { - // handle arrays differnt - if part.contains("[]") { - let part_array: &str = &part[..part.len()-2]; - - let v: Vec = Vec::new(); - - // // ... fill in the vec with some Value::Object's as you like it ... - // // ... in our case its one or more strings - - let a = Value::Array(v); - // let mut map: serde_json::Map = serde_json::Map::new(); - // map.insert("person",a); - // let o = Value::Object(map); - - let mut new_object = Map::new(); - new_object.insert(part_array.to_string(),a); - current_value = Value::Object(new_object); - } - else { - let mut new_object = Map::new(); - new_object.insert(part.to_string(), current_value); - current_value = Value::Object(new_object); - - } - } - - // if the end of the path indicates an array than add an array to the leaf and return the array to be filled with a value - - - current_value -} - pub fn merge(a: &mut Value, b: Value) { // todo: here anything non object is actually simply overwritten. So here we need to introduce type checking of the Value b. match (a, b) { From 1fc6af395e0c164dab41803e36609d4613e16ed9 Mon Sep 17 00:00:00 2001 From: Oran Dan Date: Wed, 18 Sep 2024 15:56:11 +0200 Subject: [PATCH 12/45] fix: improve JsonPath to Pointer fn, fix array constructing and merging --- example_OBv3_to_ELM.json | 26 ++++---- .../custom_mapping_OBv3_ELM_latest copy.json | 2 +- .../custom_mapping_OBv3_ELM_latest.json | 41 +++++++------ src/backend/jsonpointer.rs | 15 ++++- src/backend/leaf_nodes.rs | 29 ++++----- src/backend/repository.rs | 60 +++++++++++-------- 6 files changed, 97 insertions(+), 76 deletions(-) diff --git a/example_OBv3_to_ELM.json b/example_OBv3_to_ELM.json index 6815bf7..e2a6e2c 100644 --- a/example_OBv3_to_ELM.json +++ b/example_OBv3_to_ELM.json @@ -14,20 +14,24 @@ } ], "credentialSubject": { - "hasClaim[0]": { - "id": "urn:epass:learningAchievement:2", - "specifiedBy": { - "learningOutcome": {}, - "title": { - "en": { - "[0]": "Title of Achievement" + "hasClaim": [ + { + "id": "urn:epass:learningAchievement:2", + "specifiedBy": { + "learningOutcome": {}, + "title": { + "en": [ + "Title of Achievement" + ] } + }, + "title": { + "en": [ + "TITLE OF PROGRAMME" + ] } - }, - "title": { - "en[0]": "TITLE OF PROGRAMME" } - }, + ], "id": "did:key:afsdlkj34134" }, "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest copy.json b/json/mapping/custom_mapping_OBv3_ELM_latest copy.json index a2b2b85..e76e371 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest copy.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest copy.json @@ -73,7 +73,7 @@ }, "destination": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].specifiedBy.title.en.[0]" + "path": "$.credentialSubject.hasClaim[0].specifiedBy.title.en[0]" } }, { diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index 4c6a1c5..aae4cd5 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -58,89 +58,88 @@ "type_": "copy", "source": { "format": "OBv3", - "path": "$.credentialSubject.achievement.id" + "path": "$.validFrom" }, "destination": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].id" + "path": "$.validFrom" } }, { "type_": "copy", "source": { "format": "OBv3", - "path": "$.credentialSubject.achievement.name" - }, + "path": "$.credentialSubject.id" + }, "destination": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].specifiedBy.title.en.[0]" + "path": "$.credentialSubject.id" } }, { "type_": "copy", "source": { "format": "OBv3", - "path": "$.validFrom" + "path": "$.proof" }, "destination": { "format": "ELM", - "path": "$.validFrom" + "path": "$.proof" } }, { "type_": "copy", "source": { "format": "OBv3", - "path": "$.name" + "path": "$.credentialSchema" }, "destination": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].title.en[0]" + "path": "$.credentialSchema" } }, { - "type_": "copy", + "type_": "markdownToJson", "source": { "format": "OBv3", - "path": "$.credentialSubject.id" + "path": "$.credentialSubject.achievement.criteria.narrative" }, "destination": { "format": "ELM", - "path": "$.credentialSubject.id" + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" } }, { "type_": "copy", "source": { "format": "OBv3", - "path": "$.proof" + "path": "$.name" }, "destination": { "format": "ELM", - "path": "$.proof" + "path": "$.credentialSubject.hasClaim[0].title.en[0]" } }, { "type_": "copy", "source": { "format": "OBv3", - "path": "$.credentialSchema" + "path": "$.credentialSubject.achievement.id" }, "destination": { "format": "ELM", - "path": "$.credentialSchema" + "path": "$.credentialSubject.hasClaim[0].id" } }, { - "type_": "markdownToJson", + "type_": "copy", "source": { "format": "OBv3", - "path": "$.credentialSubject.achievement.criteria.narrative" - }, + "path": "$.credentialSubject.achievement.name" + }, "destination": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" + "path": "$.credentialSubject.hasClaim[0].specifiedBy.title.en[0]" } } - ] \ No newline at end of file diff --git a/src/backend/jsonpointer.rs b/src/backend/jsonpointer.rs index 1106077..15bb736 100644 --- a/src/backend/jsonpointer.rs +++ b/src/backend/jsonpointer.rs @@ -1,5 +1,7 @@ use std::ops::Deref; +use regex::Regex; + #[derive(Debug)] // TODO: add validation pub struct JsonPath(pub String); @@ -33,6 +35,17 @@ impl TryFrom for JsonPointer { type Error = String; fn try_from(value: JsonPath) -> Result { - Ok(JsonPointer(value.0.trim_start_matches('$').replace('.', "/"))) + Ok(JsonPointer({ + let mut value = value.0.trim_start_matches('$').replace('.', "/"); + + // Check if the JsonPath contains arrays and convert them as well + let regx = Regex::new(r"\[(\d+)\]").unwrap(); + + while regx.is_match(&value) { + value = regx.replace(&value, "/$1").to_string(); + } + + value + })) } } diff --git a/src/backend/leaf_nodes.rs b/src/backend/leaf_nodes.rs index e9a0d40..c4494d4 100644 --- a/src/backend/leaf_nodes.rs +++ b/src/backend/leaf_nodes.rs @@ -32,26 +32,23 @@ pub fn construct_leaf_node(path: &str) -> Value { // Split the input string by '/' and filter out any empty parts let parts: Vec<&str> = path.split('/').filter(|&s| !s.is_empty()).collect(); - // Initialize the root of the JSON structure as null + // Initialize the leaf_node value to Null, the actual value will be inserted later by the apply_transformation function let mut current_value = Value::Null; // Iterate through the parts in reverse order to build the nested structure for part in parts.into_iter().rev() { - // handle arrays differnt - if part.contains("[]") { // todo: this should include the number ofcourse - let part_array: &str = &part[..part.len()-2]; - - let v: Vec = Vec::new(); - - // // ... fill in the vec with some Value::Object's as you like it ... - // // ... in our case its one or more strings - - let a = Value::Array(v); - - let mut new_object = Map::new(); - new_object.insert(part_array.to_string(),a); - current_value = Value::Object(new_object); + // Check if the part is an array index + if part.chars().all(|c| c.is_numeric()) { + if let Ok(part_array_index) = part.parse::() { + let mut new_array = Vec::new(); + for i in 0..part_array_index { + new_array.insert(i, Value::Null); + } + + new_array.insert(part_array_index, current_value); + current_value = Value::Array(new_array); + } } else { let mut new_object = Map::new(); @@ -61,7 +58,5 @@ pub fn construct_leaf_node(path: &str) -> Value { } } - // if the end of the path indicates an array than add an array to the leaf and return the array to be filled with a value - current_value } diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 2765ace..f06e5a1 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -1,17 +1,19 @@ use crate::{ backend::{ - jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, transformations::{DataLocation, StringValue, Transformation} + jsonpointer::{JsonPath, JsonPointer}, + leaf_nodes::construct_leaf_node, + transformations::{DataLocation, StringValue, Transformation}, }, state::{AppState, Mapping}, trace_dbg, }; use jsonpath_rust::JsonPathFinder; +use regex::Regex; use serde_json::{json, Value}; use std::{ collections::HashMap, ops::{Deref, DerefMut}, }; -use regex::Regex; #[derive(Debug, Default, Clone)] pub struct Repository(HashMap); @@ -78,6 +80,7 @@ impl Repository { }; let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); let mut leaf_node = construct_leaf_node(&pointer); @@ -129,10 +132,7 @@ impl Repository { Transformation::StringToOne { type_: transformation, - source: - StringValue { - value: source_value, - }, + source: StringValue { value: source_value }, destination: DataLocation { format: destination_format, @@ -143,7 +143,7 @@ impl Repository { return None; } - let dest = destination_path.clone(); + let dest = destination_path.clone(); let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. let pointer = JsonPointer::try_from(JsonPath(destination_path)).unwrap(); @@ -152,14 +152,13 @@ impl Repository { // Deserialize the JSON string into a MyStruct // remove the array element from the destantation and reuse this new pointer // fill the array with the new value - let array_pointer = &pointer[..pointer.len()-2]; - if let Some(array) = leaf_node.pointer_mut(&array_pointer) .and_then(|v| v.as_array_mut()) { + let array_pointer = &pointer[..pointer.len() - 2]; + if let Some(array) = leaf_node.pointer_mut(&array_pointer).and_then(|v| v.as_array_mut()) { // Push a new string to the array - array.push(Value::String(source_value)); + array.push(Value::String(source_value)); } - } - else if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(source_value); + } else if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(source_value); } merge(destination_credential, leaf_node); @@ -203,7 +202,6 @@ impl Repository { // run the source value through a markdown converter to fit the nested objects into a markdown string let markdown_source_value = json!(json_to_markdown(&source_value, 0)); - if let Some(value) = leaf_node.pointer_mut(&pointer) { *value = transformation.apply(markdown_source_value); } @@ -243,7 +241,6 @@ impl Repository { } }; - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); @@ -252,7 +249,6 @@ impl Repository { // run the source value through a markdown converter to fit the nested objects into a markdown string let json_source_value = json!(markdown_to_json(&source_value.to_string())); - if let Some(value) = leaf_node.pointer_mut(&pointer) { *value = transformation.apply(json_source_value); } @@ -263,9 +259,6 @@ impl Repository { Some((destination_path, source_path)) } - - - _ => todo!(), } } @@ -298,14 +291,29 @@ impl Repository { } pub fn merge(a: &mut Value, b: Value) { - // todo: here anything non object is actually simply overwritten. So here we need to introduce type checking of the Value b. match (a, b) { (a @ &mut Value::Object(_), Value::Object(b)) => { let a = a.as_object_mut().unwrap(); for (k, v) in b { - merge(a.entry(k).or_insert(Value::Null), v); // + merge(a.entry(k).or_insert(Value::Null), v); } } + (a @ &mut Value::Array(_), Value::Array(b_arr)) => { + let a_arr = a.as_array_mut().unwrap(); + let a_len = a_arr.len(); + let b_iter = b_arr.into_iter(); + + for (i, b_val) in b_iter.enumerate() { + if i < a_len { + merge(&mut a_arr[i], b_val); + } else { + a_arr.push(b_val); + } + } + } + (_, Value::Null) => { + // If the incoming merge Json Value is `Value::Null`, do nothing to the existing Json Value,the current repository, `a` + } (a, b) => *a = b, } } @@ -361,7 +369,11 @@ fn json_to_markdown(json: &Value, indent_level: usize) -> String { } Value::Array(arr) => { for item in arr { - markdown.push_str(&format!("{}- {}\n", indent, json_to_markdown(item, indent_level + 1).trim())); + markdown.push_str(&format!( + "{}- {}\n", + indent, + json_to_markdown(item, indent_level + 1).trim() + )); } } Value::String(s) => { @@ -381,8 +393,6 @@ fn json_to_markdown(json: &Value, indent_level: usize) -> String { markdown } - - fn markdown_to_json(markdown: &str) -> Value { let mut lines = markdown.lines().peekable(); let mut current_indent = 0; @@ -477,4 +487,4 @@ fn markdown_to_json(markdown: &str) -> Value { // 4. Regex Patterns: // • heading_regex: Matches Markdown headings (e.g., # Title). // • bold_regex: Matches bolded keys (e.g., **Key**:). -// • list_item_regex: Matches list items (e.g., - item). \ No newline at end of file +// • list_item_regex: Matches list items (e.g., - item). From db71ccf3eb327a0b211b0c3e017f1291033a8bf2 Mon Sep 17 00:00:00 2001 From: Oran Dan Date: Wed, 18 Sep 2024 16:12:36 +0200 Subject: [PATCH 13/45] chore: cargo fmt & clippy --- src/backend/candidate_value.rs | 9 +++------ src/backend/jsonpointer.rs | 2 +- src/backend/leaf_nodes.rs | 5 +---- src/backend/repository.rs | 29 ++++++++++++++++------------- src/backend/transformations.rs | 5 +---- 5 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/backend/candidate_value.rs b/src/backend/candidate_value.rs index 73af0d9..4a8a1fc 100644 --- a/src/backend/candidate_value.rs +++ b/src/backend/candidate_value.rs @@ -9,7 +9,6 @@ use crate::{ trace_dbg, }; - pub fn set_candidate_output_value(state: &mut AppState, push_transformation: bool) { // todo: is it needed to add directcopy to the start? let selected_transformations = [ @@ -48,7 +47,7 @@ pub fn define_transformation(state: &mut AppState, transformation: Transformatio let input_value: String = state.input_fields[state.selected_input_field].0.clone(); let destination_path: JsonPath = JsonPointer(state.output_pointer.clone()).into(); - let transformation = match transformation { + match transformation { Transformations::LowerCase => Transformation::OneToOne { type_: OneToOne::toLowerCase, source: DataLocation { @@ -102,7 +101,7 @@ pub fn define_transformation(state: &mut AppState, transformation: Transformatio Transformations::StringToOne => Transformation::StringToOne { type_: StringToOne::stringit, source: StringValue { - value: input_value.clone() + value: input_value.clone(), }, destination: DataLocation { format: output_format.clone(), @@ -120,9 +119,7 @@ pub fn define_transformation(state: &mut AppState, transformation: Transformatio path: destination_path.to_string(), }, }, - }; - let transformation = transformation; - transformation + } } pub fn set_output_pointer(state: &mut AppState) { diff --git a/src/backend/jsonpointer.rs b/src/backend/jsonpointer.rs index 15bb736..15c6a52 100644 --- a/src/backend/jsonpointer.rs +++ b/src/backend/jsonpointer.rs @@ -37,7 +37,7 @@ impl TryFrom for JsonPointer { fn try_from(value: JsonPath) -> Result { Ok(JsonPointer({ let mut value = value.0.trim_start_matches('$').replace('.', "/"); - + // Check if the JsonPath contains arrays and convert them as well let regx = Regex::new(r"\[(\d+)\]").unwrap(); diff --git a/src/backend/leaf_nodes.rs b/src/backend/leaf_nodes.rs index c4494d4..e4b6bff 100644 --- a/src/backend/leaf_nodes.rs +++ b/src/backend/leaf_nodes.rs @@ -35,7 +35,6 @@ pub fn construct_leaf_node(path: &str) -> Value { // Initialize the leaf_node value to Null, the actual value will be inserted later by the apply_transformation function let mut current_value = Value::Null; - // Iterate through the parts in reverse order to build the nested structure for part in parts.into_iter().rev() { // Check if the part is an array index @@ -49,12 +48,10 @@ pub fn construct_leaf_node(path: &str) -> Value { new_array.insert(part_array_index, current_value); current_value = Value::Array(new_array); } - } - else { + } else { let mut new_object = Map::new(); new_object.insert(part.to_string(), current_value); current_value = Value::Object(new_object); - } } diff --git a/src/backend/repository.rs b/src/backend/repository.rs index f06e5a1..f56d040 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -11,6 +11,7 @@ use jsonpath_rust::JsonPathFinder; use regex::Regex; use serde_json::{json, Value}; use std::{ + cmp::Ordering, collections::HashMap, ops::{Deref, DerefMut}, }; @@ -153,7 +154,7 @@ impl Repository { // remove the array element from the destantation and reuse this new pointer // fill the array with the new value let array_pointer = &pointer[..pointer.len() - 2]; - if let Some(array) = leaf_node.pointer_mut(&array_pointer).and_then(|v| v.as_array_mut()) { + if let Some(array) = leaf_node.pointer_mut(array_pointer).and_then(|v| v.as_array_mut()) { // Push a new string to the array array.push(Value::String(source_value)); } @@ -394,7 +395,7 @@ fn json_to_markdown(json: &Value, indent_level: usize) -> String { } fn markdown_to_json(markdown: &str) -> Value { - let mut lines = markdown.lines().peekable(); + let lines = markdown.lines().peekable(); let mut current_indent = 0; let mut stack: Vec = vec![Value::Object(Default::default())]; let mut current_key: Option = None; @@ -403,7 +404,7 @@ fn markdown_to_json(markdown: &str) -> Value { let bold_regex = Regex::new(r"^\s*\*\*(.+?)\*\*\s*:$").unwrap(); let list_item_regex = Regex::new(r"^\s*-\s*(.+)").unwrap(); - while let Some(line) = lines.next() { + for line in lines { let line_indent = line.chars().take_while(|c| c.is_whitespace()).count(); let line = line.trim(); @@ -412,18 +413,20 @@ fn markdown_to_json(markdown: &str) -> Value { } // Adjust stack based on indentation - if line_indent > current_indent { - stack.push(Value::Object(Default::default())); - } else if line_indent < current_indent { - let value = stack.pop().unwrap(); - let parent = stack.last_mut().unwrap(); - if let Some(key) = current_key.take() { - if let Value::Object(ref mut obj) = parent { - obj.insert(key, value); + match line_indent.cmp(¤t_indent) { + Ordering::Greater => stack.push(Value::Object(Default::default())), + Ordering::Less => { + let value = stack.pop().unwrap(); + let parent = stack.last_mut().unwrap(); + if let Some(key) = current_key.take() { + if let Value::Object(ref mut obj) = parent { + obj.insert(key, value); + } + } else if let Value::Array(ref mut arr) = parent { + arr.push(value); } - } else if let Value::Array(ref mut arr) = parent { - arr.push(value); } + _ => {} } current_indent = line_indent; diff --git a/src/backend/transformations.rs b/src/backend/transformations.rs index b499b1a..c7831b4 100644 --- a/src/backend/transformations.rs +++ b/src/backend/transformations.rs @@ -109,7 +109,6 @@ impl StringToOne { } } - #[allow(non_camel_case_types)] #[derive(Serialize, Deserialize, Debug, Clone)] pub enum JsonToMarkdown { @@ -138,8 +137,6 @@ impl MarkdownToJson { } } - - #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum Transformation { @@ -184,4 +181,4 @@ pub struct DataLocation { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct StringValue { pub value: String, -} \ No newline at end of file +} From 77a28dc2618445a898f361d0145958aa3d3291ec Mon Sep 17 00:00:00 2001 From: hamrt Date: Wed, 18 Sep 2024 17:30:19 +0200 Subject: [PATCH 14/45] add testing capacity --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index df677f0..e84058e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ logging_folder +.vscode/launch.json From 7743fbf260c6356c995601bda17c334dd06b3199 Mon Sep 17 00:00:00 2001 From: hamrt Date: Tue, 24 Sep 2024 09:12:35 +0200 Subject: [PATCH 15/45] added markdown to json --- example_ELM-to-OBv3.json | 2 +- example_OBv3_to_ELM.json | 27 +- .../custom_mapping_ELM_OBv3_latest.json | 8 +- .../custom_mapping_OBv3_ELM_latest copy.json | 2 +- .../custom_mapping_OBv3_ELM_latest.json | 2 +- src/backend/repository.rs | 334 ++++++++++++------ 6 files changed, 264 insertions(+), 111 deletions(-) diff --git a/example_ELM-to-OBv3.json b/example_ELM-to-OBv3.json index 9f09aa9..6f86dd2 100644 --- a/example_ELM-to-OBv3.json +++ b/example_ELM-to-OBv3.json @@ -16,7 +16,7 @@ "credentialSubject": { "achievement": { "criteria": { - "narrative": "- **id**:\n urn:epass:learningOutcome:2\n\n **relatedSkill**:\n - **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo\n\n **inScheme**:\n **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/25831c2\n\n **type**:\n ConceptScheme\n\n\n **prefLabel**:\n **en**:\n - 5.4 Identifying digital competence gaps\n\n\n **type**:\n Concept\n\n **title**:\n **en**:\n - Name of DigiComp Competence\n\n\n **type**:\n LearningOutcome\n- **id**:\n urn:epass:learningOutcome:3\n\n **relatedSkill**:\n - **id**:\n http://data.europa.eu/snb/dcf/34v10n662m\n\n **inScheme**:\n **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/25831c2\n\n **type**:\n ConceptScheme\n\n\n **prefLabel**:\n **en**:\n - 3.1 Proficiency Level Foundation 2\n\n\n **type**:\n Concept\n\n **title**:\n **en**:\n - Name of DigiComp Competence 2\n\n\n **type**:\n LearningOutcome\n" + "narrative": "**id**:\n urn:epass:learningOutcome:2\n**relatedSkill**:\n - **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo\n **inScheme**:\n **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/25831c2\n **type**:\n ConceptScheme\n **prefLabel**:\n **en**:\n - 5.4 Identifying digital competence gaps\n **type**:\n Concept\n**title**:\n **en**:\n - Name of DigiComp Competence\n**type**:\n LearningOutcome\n" }, "id": "urn:epass:learningAchievement:2", "name": "Title of Achievement", diff --git a/example_OBv3_to_ELM.json b/example_OBv3_to_ELM.json index e2a6e2c..e1af948 100644 --- a/example_OBv3_to_ELM.json +++ b/example_OBv3_to_ELM.json @@ -18,7 +18,32 @@ { "id": "urn:epass:learningAchievement:2", "specifiedBy": { - "learningOutcome": {}, + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:2", + "relatedSkill": [ + { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "5.4 Identifying digital competence gaps" + ] + }, + "type": "Concept" + } + ], + "title": { + "en": [ + "Name of DigiComp Competence" + ] + }, + "type": "LearningOutcome" + } + ], "title": { "en": [ "Title of Achievement" diff --git a/json/mapping/custom_mapping_ELM_OBv3_latest.json b/json/mapping/custom_mapping_ELM_OBv3_latest.json index 3534b17..7ffd394 100644 --- a/json/mapping/custom_mapping_ELM_OBv3_latest.json +++ b/json/mapping/custom_mapping_ELM_OBv3_latest.json @@ -50,7 +50,7 @@ }, "destination": { "format": "OBv3", - "path": "$.issuer.type[]" + "path": "$.issuer.type[0]" } }, { @@ -82,7 +82,7 @@ }, "destination": { "format": "OBv3", - "path": "$.credentialSubject.achievement.type[]" + "path": "$.credentialSubject.achievement.type[0]" } }, { @@ -147,7 +147,7 @@ }, "destination": { "format": "OBv3", - "path": "$.credentialSubject.type[]" + "path": "$.credentialSubject.type[0]" } }, { @@ -176,7 +176,7 @@ "type_": "jsonToMarkdown", "source": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome[0]" }, "destination": { "format": "OBv3", diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest copy.json b/json/mapping/custom_mapping_OBv3_ELM_latest copy.json index e76e371..6e7047d 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest copy.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest copy.json @@ -128,7 +128,7 @@ }, "destination": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome[0]" } } diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index aae4cd5..08721ae 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -106,7 +106,7 @@ }, "destination": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome[0]" } }, { diff --git a/src/backend/repository.rs b/src/backend/repository.rs index f56d040..9c06c41 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -8,10 +8,9 @@ use crate::{ trace_dbg, }; use jsonpath_rust::JsonPathFinder; -use regex::Regex; -use serde_json::{json, Value}; +use serde_json::{json, Value, Map}; +use tracing::debug; use std::{ - cmp::Ordering, collections::HashMap, ops::{Deref, DerefMut}, }; @@ -144,21 +143,11 @@ impl Repository { return None; } - let dest = destination_path.clone(); let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. let pointer = JsonPointer::try_from(JsonPath(destination_path)).unwrap(); let mut leaf_node = construct_leaf_node(&pointer); - if dest.contains("[]") { - // Deserialize the JSON string into a MyStruct - // remove the array element from the destantation and reuse this new pointer - // fill the array with the new value - let array_pointer = &pointer[..pointer.len() - 2]; - if let Some(array) = leaf_node.pointer_mut(array_pointer).and_then(|v| v.as_array_mut()) { - // Push a new string to the array - array.push(Value::String(source_value)); - } - } else if let Some(value) = leaf_node.pointer_mut(&pointer) { + if let Some(value) = leaf_node.pointer_mut(&pointer) { *value = transformation.apply(source_value); } @@ -247,13 +236,24 @@ impl Repository { let mut leaf_node = construct_leaf_node(&pointer); - // run the source value through a markdown converter to fit the nested objects into a markdown string - let json_source_value = json!(markdown_to_json(&source_value.to_string())); - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(json_source_value); + if let Some(inner_string) = &source_value.as_str() { + let mut lines: Vec<&str> = inner_string.lines().collect(); + + lines.insert(0, ""); + + // Split the string by newlines and collect into Vec<&str> + let markdown_function_result = markdown_to_json(&lines); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(markdown_function_result); + } } + + + + merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); @@ -358,126 +358,254 @@ fn remove_key_recursive(current_json: &mut Value, keys: &[String]) -> bool { fn json_to_markdown(json: &Value, indent_level: usize) -> String { let mut markdown = String::new(); - let indent = " ".repeat(indent_level); + let indent = " ".repeat(indent_level); match json { + // Handle JSON objects (key-value pairs) Value::Object(map) => { for (key, value) in map { + // Add the key as a bold label markdown.push_str(&format!("{}**{}**:\n", indent, key)); + // Recursively handle the value markdown.push_str(&json_to_markdown(value, indent_level + 1)); - markdown.push('\n'); } } - Value::Array(arr) => { - for item in arr { - markdown.push_str(&format!( - "{}- {}\n", - indent, - json_to_markdown(item, indent_level + 1).trim() - )); + + // Handle JSON arrays + Value::Array(array) => { + for item in array { + // Add each array item as a list item + markdown.push_str(&format!("{}- ", indent)); + markdown.push_str(&json_to_markdown(item, indent_level + 1)); } } - Value::String(s) => { - markdown.push_str(&format!("{}{}\n", indent, s)); - } - Value::Number(n) => { - markdown.push_str(&format!("{}{}\n", indent, n)); - } - Value::Bool(b) => { - markdown.push_str(&format!("{}{}\n", indent, b)); - } - Value::Null => { - markdown.push_str(&format!("{}null\n", indent)); - } + + // Handle primitive types: strings, numbers, booleans, and null + Value::String(s) => markdown.push_str(&format!("{}{}\n", indent, s)), + Value::Number(n) => markdown.push_str(&format!("{}{}\n", indent, n)), + Value::Bool(b) => markdown.push_str(&format!("{}{}\n", indent, b)), + Value::Null => markdown.push_str(&format!("{}null\n", indent)), } markdown } -fn markdown_to_json(markdown: &str) -> Value { - let lines = markdown.lines().peekable(); - let mut current_indent = 0; - let mut stack: Vec = vec![Value::Object(Default::default())]; - let mut current_key: Option = None; - let heading_regex = Regex::new(r"^#+ (.+)").unwrap(); - let bold_regex = Regex::new(r"^\s*\*\*(.+?)\*\*\s*:$").unwrap(); - let list_item_regex = Regex::new(r"^\s*-\s*(.+)").unwrap(); - for line in lines { - let line_indent = line.chars().take_while(|c| c.is_whitespace()).count(); - let line = line.trim(); - if line.is_empty() { - continue; +/// Recursively converts indented lines of Markdown into a JSON structure. +fn markdown_to_json(lines: &[&str]) -> Value { + let mut i = 0; + let mut position: Vec = Vec::new(); + + //lets create a string to which we will concatenate new lines based on the markdown lines + position.insert(0, "".to_string()); + let mut json_string = String::from(""); + + while i < lines.len() { + let line = lines[i]; + + // Handle key-value pairs (e.g., **key**: value) + let (obj_type, depth) = evaluate_line(line); + if obj_type == "E" && i == 0 { + // open a json object + json_string.push_str("{\n"); } - // Adjust stack based on indentation - match line_indent.cmp(¤t_indent) { - Ordering::Greater => stack.push(Value::Object(Default::default())), - Ordering::Less => { - let value = stack.pop().unwrap(); - let parent = stack.last_mut().unwrap(); - if let Some(key) = current_key.take() { - if let Value::Object(ref mut obj) = parent { - obj.insert(key, value); + if obj_type == "O" { + while depth < position.len() - 1 { + // we need to close the positions + if let Some(last_value) = position.last() { + if last_value == "O" { + // The last value is O we can now close this object + if let Some(_last_value) = position.pop() { + json_string.pop(); + json_string = json_string.trim_end_matches(',').to_string(); + json_string.push_str("},\n"); + } + } else if last_value == "A" { + // close value A + if let Some(_last_value) = position.pop() { + json_string.push_str("]\n"); + } + } else if last_value == "OA" && depth < position.len() - 1 { + //The last value is OA + if let Some(_last_value) = position.pop() { + // first close the array + json_string.pop(); + json_string = json_string.trim_end_matches(',').to_string(); + json_string.push_str("],\n"); + if depth > 0 { + if let Some(_last_value) = position.pop() { + // then close the object + json_string.push_str("},\n"); + } + } else { + //"The vector was empty, nothing to remove."); + } + } else { + // ("The vector was empty, nothing to remove."); + } + } else { + // we have a different last value we will remove it from tha vector + if let Some(_last_value) = position.pop() { + // json_string.push_str("]\n"); + } } - } else if let Value::Array(ref mut arr) = parent { - arr.push(value); + } else { + // println!("The vector is empty."); } } - _ => {} - } - - current_indent = line_indent; - if let Some(caps) = heading_regex.captures(line) { - let heading = caps.get(1).unwrap().as_str().trim().to_string(); - current_key = Some(heading); - } else if let Some(caps) = bold_regex.captures(line) { - let key = caps.get(1).unwrap().as_str().trim().to_string(); - let parent = stack.last_mut().unwrap(); - if let Value::Object(ref mut obj) = parent { - obj.insert(key.clone(), Value::Null); + // setup the vector array for the right value and position + if depth >= position.len() - 1 && i >0 { + //test previous line to see is we might have a nested object + let (last_obj_type, _new_depth) = evaluate_line(lines[i - 1]); + if last_obj_type == "O" { + json_string.push('{'); + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + position.insert(depth, "O".to_string()); + } else if let Some(last_value) = position.last() { + if last_value == "OA" && depth == position.len() - 1 { + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + } else if depth > position.len() - 1 { + json_string.push('{'); + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + position.insert(depth, "O".to_string()); + } else { + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + position[depth] = "O".to_string(); + } + } + } + } else if obj_type == "A" { + while depth < position.len() - 1 { + // we need to close the positions + if let Some(last_value) = position.last() { + if last_value == "O" { + if let Some(_last_value) = position.pop() { + json_string.push_str("},\n"); + } + } else if last_value == "A" { + if let Some(_last_value) = position.pop() { + json_string.push_str("]\n"); + } + } else { + // The last value is something leave it + } + } else { + // The vector is empty. + } } - current_key = Some(key); - } else if let Some(caps) = list_item_regex.captures(line) { - let item = caps.get(1).unwrap().as_str().trim().to_string(); - let parent = stack.last_mut().unwrap(); - if let Value::Array(ref mut arr) = parent { - arr.push(Value::String(item)); - } else { - let arr = vec![Value::String(item)]; - stack.push(Value::Array(arr)); + + // setup the vector array for the right value and position + if depth >= position.len() - 1 { + if let Some(last_value) = position.last() { + if last_value == "OA" { + json_string.push('['); + } else if last_value == "A" && depth == position.len() - 1 { + position.insert(depth, "A".to_string()); + } else { + position.insert(depth, "A".to_string()); + json_string.push('['); + } + } + json_string.push_str(&cleanup_string(line)); } - } else { - let parent = stack.last_mut().unwrap(); - if let Some(key) = current_key.take() { - if let Value::Object(ref mut obj) = parent { - obj.insert(key, Value::String(line.to_string())); + } else if obj_type == "OA" { + while depth < position.len() - 1 { + // we need to close the positions + if let Some(last_value) = position.last() { + if last_value == "O" { + if let Some(_last_value) = position.pop() { + json_string.pop(); + json_string.pop(); + json_string.push_str("},\n"); + } + } else if last_value == "A" { + if let Some(_last_value) = position.pop() { + json_string.push_str("]\n"); + } else { + // The vector was empty, nothing to remove. + } + } else { + // The last value is something else leave it + } + } else { + // The vector is empty } - } else if let Value::Array(ref mut arr) = parent { - arr.push(Value::String(line.to_string())); } + + // we are creating a new array that will contain objects of the same type + json_string.push_str("[ \n {"); + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + // test if extra handling is needed for closing + position.insert(depth - 1, "OA".to_string()); + position.insert(depth, "O".to_string()); + } else if obj_type == "V" { + json_string.push_str(&cleanup_string(line)); + json_string.push_str(",\n"); } + + i += 1; } - // Handle remaining items in the stack - while stack.len() > 1 { - let value = stack.pop().unwrap(); - let parent = stack.last_mut().unwrap(); - if let Some(key) = current_key.take() { - if let Value::Object(ref mut obj) = parent { - obj.insert(key, value); - } - } else if let Value::Array(ref mut arr) = parent { - arr.push(value); - } + // Finalize the string to which we will concatenate new lines based on the markdown lines + json_string.pop(); + json_string = json_string.trim_end_matches(',').to_string(); + json_string.push_str("\n}"); + let parsed_json: Value = serde_json::from_str(&json_string).unwrap(); + parsed_json +} + +fn evaluate_line(line_to_test: &str) -> (String, usize) { + //test depth + let mut depth = line_to_test + .chars() + .take_while(|c| c.is_whitespace()) + .count() + / 2; + //test type + let mut line_type = ""; + let trimmed = line_to_test.trim(); + if line_to_test.is_empty() { + // Handle list as object items of previous depth + line_type = "E"; + } else if trimmed.starts_with("-") && trimmed.ends_with("**:") { + // Handle list as object items of previous depth + line_type = "OA"; + depth += 1; + } else if trimmed.starts_with("**") { + // Handle list as object items of previous depth + line_type = "O"; + } else if trimmed.starts_with("-") { + // Handle as array items of previous depth + line_type = "A"; + } else { + // Handle value of previous depth + line_type = "V"; } - stack.pop().unwrap() + (line_type.to_string(), depth) } +fn cleanup_string(string_to_clean: &str) -> String { + //trim the string + let string_to_clean1 = string_to_clean.trim(); + let string_to_clean2 = string_to_clean1.replace("-", ""); + let string_to_clean3 = string_to_clean2.trim(); + let string_to_clean4 = string_to_clean3.replace("**:", ""); + let cleaned_string = string_to_clean4.trim().trim_matches('*').to_string(); + // Add quotes around the cleaned string + format!("\"{}\"", cleaned_string) +} + + // 1. Parsing Markdown: // • Headings (#): These are treated as keys in the resulting JSON object. // • Bold Text (**): This is also treated as a key in the JSON object. From 05b341fc32d9cc75d037b3048b7a6c6cc9f18102 Mon Sep 17 00:00:00 2001 From: hamrt Date: Tue, 24 Sep 2024 09:25:20 +0200 Subject: [PATCH 16/45] removed unsed variables --- src/backend/repository.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 9c06c41..7fa3cb3 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -8,8 +8,7 @@ use crate::{ trace_dbg, }; use jsonpath_rust::JsonPathFinder; -use serde_json::{json, Value, Map}; -use tracing::debug; +use serde_json::{json, Value}; use std::{ collections::HashMap, ops::{Deref, DerefMut}, @@ -571,7 +570,7 @@ fn evaluate_line(line_to_test: &str) -> (String, usize) { .count() / 2; //test type - let mut line_type = ""; + let line_type; let trimmed = line_to_test.trim(); if line_to_test.is_empty() { // Handle list as object items of previous depth From 9a6c508c0121cd8df26b96f3ee9f7b68cfe495e5 Mon Sep 17 00:00:00 2001 From: hamrt Date: Wed, 9 Oct 2024 16:15:02 +0200 Subject: [PATCH 17/45] fix for headless work and credentialSchema translation --- .gitignore | 2 + example_ELM_to_OBv3.json | 49 +++++++++++++++++++ example_OBv3_to_ELM.json | 6 +-- example_ebsi-obv3.json | 49 +++++++++++++++++++ .../custom_mapping_ELM_OBv3_latest.json | 39 +++++++++++++-- src/backend/headless_cli.rs | 2 + src/backend/init_conversion.rs | 33 ++++++++++++- 7 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 example_ELM_to_OBv3.json create mode 100644 example_ebsi-obv3.json diff --git a/.gitignore b/.gitignore index e84058e..43a3458 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ target/ logging_folder .vscode/launch.json +json/.DS_Store +.DS_Store diff --git a/example_ELM_to_OBv3.json b/example_ELM_to_OBv3.json new file mode 100644 index 0000000..b7301bc --- /dev/null +++ b/example_ELM_to_OBv3.json @@ -0,0 +1,49 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://accrediter.edu/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialSubject": { + "achievement": { + "criteria": { + "narrative": "**id**:\n urn:epass:learningOutcome:2\n**relatedSkill**:\n - **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo\n **inScheme**:\n **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/25831c2\n **type**:\n ConceptScheme\n **prefLabel**:\n **en**:\n - 5.4 Identifying digital competence gaps\n **type**:\n Concept\n**title**:\n **en**:\n - Name of DigiComp Competence\n**type**:\n LearningOutcome\n" + }, + "id": "urn:epass:learningAchievement:2", + "name": "Title of Achievement", + "type": [ + "Achievement" + ] + }, + "id": "did:key:afsdlkj34134", + "type": [ + "AchievementSubject" + ] + }, + "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", + "issuer": { + "address": { + "addressCountryCode": "http://publications.europa.eu/resource/authority/country/ESP" + }, + "id": "did:ebsi:org:12345689", + "name": "ORGANIZACION TEST", + "type": [ + "Profile" + ] + }, + "name": "TITLE OF PROGRAMME", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "validFrom": "2019-09-20T00:00:00+02:00" +} \ No newline at end of file diff --git a/example_OBv3_to_ELM.json b/example_OBv3_to_ELM.json index e1af948..0663c2e 100644 --- a/example_OBv3_to_ELM.json +++ b/example_OBv3_to_ELM.json @@ -1,7 +1,6 @@ { "@context": [ - "https://www.w3.org/ns/credentials/v2", - "http://data.europa.eu/snb/model/context/edc-ap" + "https://www.w3.org/ns/credentials/v2" ], "credentialSchema": [ { @@ -68,8 +67,7 @@ }, "type": [ "VerifiableCredential", - "VerifiableAttestation", - "EuropeanDigitalCredential" + "OpenBadgeCredential" ], "validFrom": "2019-09-20T00:00:00+02:00" } \ No newline at end of file diff --git a/example_ebsi-obv3.json b/example_ebsi-obv3.json new file mode 100644 index 0000000..2bd1914 --- /dev/null +++ b/example_ebsi-obv3.json @@ -0,0 +1,49 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "http://data.europa.eu/snb/model/context/edc-ap" + ], + "credentialSchema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialSubject": { + "achievement": { + "criteria": { + "narrative": "**id**:\n urn:epass:learningOutcome:2\n**relatedSkill**:\n - **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo\n **inScheme**:\n **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/25831c2\n **type**:\n ConceptScheme\n **prefLabel**:\n **en**:\n - 5.4 Identifying digital competence gaps\n **type**:\n Concept\n**title**:\n **en**:\n - Name of DigiComp Competence\n**type**:\n LearningOutcome\n" + }, + "id": "urn:epass:learningAchievement:2", + "name": "Title of Achievement", + "type": [ + "Achievement" + ] + }, + "id": "did:key:afsdlkj34134", + "type": [ + "AchievementSubject" + ] + }, + "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", + "issuer": { + "address": { + "addressCountryCode": "http://publications.europa.eu/resource/authority/country/ESP" + }, + "id": "did:ebsi:org:12345689", + "name": "ORGANIZACION TEST", + "type": [ + "Profile" + ] + }, + "name": "TITLE OF PROGRAMME", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "validFrom": "2019-09-20T00:00:00+02:00" +} \ No newline at end of file diff --git a/json/mapping/custom_mapping_ELM_OBv3_latest.json b/json/mapping/custom_mapping_ELM_OBv3_latest.json index 7ffd394..2725cfd 100644 --- a/json/mapping/custom_mapping_ELM_OBv3_latest.json +++ b/json/mapping/custom_mapping_ELM_OBv3_latest.json @@ -22,14 +22,23 @@ } }, { - "type_": "copy", + "type_": "stringit", "source": { - "format": "ELM", - "path": "$.type" + "value": "VerifiableCredential" }, "destination": { "format": "OBv3", - "path": "$.type" + "path": "$.type[0]" + } + }, + { + "type_": "stringit", + "source": { + "value": "OpenBadgeCredential" + }, + "destination": { + "format": "OBv3", + "path": "$.type[1]" } }, { @@ -64,6 +73,28 @@ "path": "$.issuer.name" } }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuer.location[0].address.fullAddress.noteLiteral.en" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer.address.streetAddress" + } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.issuer.location[0].address.countryCode.id" + }, + "destination": { + "format": "OBv3", + "path": "$.issuer.address.addressCountryCode" + } + }, { "type_": "copy", "source": { diff --git a/src/backend/headless_cli.rs b/src/backend/headless_cli.rs index 3a76111..478255b 100644 --- a/src/backend/headless_cli.rs +++ b/src/backend/headless_cli.rs @@ -1,4 +1,5 @@ use crate::backend::init_conversion::load_mapping_file; +use crate::backend::init_conversion::init_conversion; use crate::p2_p3_common::create_output_files; use crate::state::{AppState, Mapping}; use crate::trace_dbg; @@ -54,6 +55,7 @@ pub fn run_headless(cli_args: &mut Args, state: &mut AppState) -> Result<()> { pub fn load_files_apply_transformations(state: &mut AppState) { load_input_file(state, true); load_mapping_file(state); + init_conversion(state); create_output_files(state); } diff --git a/src/backend/init_conversion.rs b/src/backend/init_conversion.rs index b43bc67..1bed45b 100644 --- a/src/backend/init_conversion.rs +++ b/src/backend/init_conversion.rs @@ -15,7 +15,7 @@ pub fn init_conversion(state: &mut AppState) { load_input_file(state, false); load_mapping_file(state); enter_fixed_context_values(state); - + enter_fixed_schema_values(state); update_display_section(state, false); } @@ -92,7 +92,10 @@ fn enter_fixed_context_values(state: &mut AppState) { let output_elm = state.repository.get_mut("ELM").unwrap().as_object_mut().unwrap(); output_elm.insert( "@context".to_string(), - Value::Array(vec![json!("https://www.w3.org/ns/credentials/v2")]), + Value::Array(vec![ + json!("https://www.w3.org/ns/credentials/v2"), + json!("http://data.europa.eu/snb/model/context/edc-ap"), + ]), ); } else if state.mapping.output_format() == "OBv3" { let output_obv3 = state.repository.get_mut("OBv3").unwrap().as_object_mut().unwrap(); @@ -104,8 +107,34 @@ fn enter_fixed_context_values(state: &mut AppState) { ]), ); } + } + +/// Enter fixed values into '@context' field, as demanded by the respective json-schema +fn enter_fixed_schema_values(state: &mut AppState) { + if state.mapping.output_format() == "ELM" { + let output_elm = state.repository.get_mut("ELM").unwrap().as_object_mut().unwrap(); + output_elm.insert( + "credentialSchema".to_string(), + Value::Array(vec![ + json!({"id": "http://data.europa.eu/snb/model/ap/edc-generic-full","type": "ShaclValidator2017"}), + json!({"id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema"})]), + ); + } else if state.mapping.output_format() == "OBv3" { + let output_obv3 = state.repository.get_mut("OBv3").unwrap().as_object_mut().unwrap(); + output_obv3.insert( + "credentialSchema".to_string(), + Value::Array(vec![ + json!({"id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", "type": "1EdTechJsonSchemaValidator2019"}), + json!({"id": "https://accrediter.edu/schema/endorsementcredential.json","type": "1EdTechJsonSchemaValidator2019"})]), + ); + } + +} + + //////// HELPERS //////// pub fn get_json(path: impl AsRef) -> Result From c270573388d7d78f8dfc194d6993443103fb0b76 Mon Sep 17 00:00:00 2001 From: hamrt Date: Wed, 9 Oct 2024 16:18:07 +0200 Subject: [PATCH 18/45] add credential Schema translation --- src/backend/init_conversion.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/backend/init_conversion.rs b/src/backend/init_conversion.rs index 1bed45b..76f4863 100644 --- a/src/backend/init_conversion.rs +++ b/src/backend/init_conversion.rs @@ -131,7 +131,6 @@ fn enter_fixed_schema_values(state: &mut AppState) { json!({"id": "https://accrediter.edu/schema/endorsementcredential.json","type": "1EdTechJsonSchemaValidator2019"})]), ); } - } From 381ab420b239dc6df315f6ced670b49ca820efe2 Mon Sep 17 00:00:00 2001 From: hamrt Date: Fri, 11 Oct 2024 12:13:18 +0200 Subject: [PATCH 19/45] add identifier mapping --- example_ELM_to_OBv3.json | 42 ++++ example_OBv3_to_ELM.json | 21 +- .../custom_mapping_ELM_OBv3_latest.json | 60 ++++++ .../custom_mapping_OBv3_ELM_latest.json | 82 ++++++-- src/backend/repository.rs | 181 +++++++++++++++++- src/backend/transformations.rs | 49 +++++ 6 files changed, 416 insertions(+), 19 deletions(-) diff --git a/example_ELM_to_OBv3.json b/example_ELM_to_OBv3.json index b7301bc..7cbd3bd 100644 --- a/example_ELM_to_OBv3.json +++ b/example_ELM_to_OBv3.json @@ -25,6 +25,48 @@ ] }, "id": "did:key:afsdlkj34134", + "identifier": [ + { + "hashed": false, + "identityHash": "545465468", + "identityType": "ext:studentID", + "salt": "not-used", + "type": "IdentityObject" + }, + { + "hashed": false, + "identityHash": { + "en": [ + "David" + ] + }, + "identityType": "ext:givenName", + "salt": "not-used", + "type": "IdentityObject" + }, + { + "hashed": false, + "identityHash": { + "en": [ + "Smith" + ] + }, + "identityType": "ext:familyName", + "salt": "not-used", + "type": "IdentityObject" + }, + { + "hashed": false, + "identityHash": { + "en": [ + "David Smith" + ] + }, + "identityType": "ext:fullName", + "salt": "not-used", + "type": "IdentityObject" + } + ], "type": [ "AchievementSubject" ] diff --git a/example_OBv3_to_ELM.json b/example_OBv3_to_ELM.json index 0663c2e..d380b9b 100644 --- a/example_OBv3_to_ELM.json +++ b/example_OBv3_to_ELM.json @@ -1,6 +1,7 @@ { "@context": [ - "https://www.w3.org/ns/credentials/v2" + "https://www.w3.org/ns/credentials/v2", + "http://data.europa.eu/snb/model/context/edc-ap" ], "credentialSchema": [ { @@ -13,6 +14,16 @@ } ], "credentialSubject": { + "fullName": { + "en": [ + "David Smith" + ] + }, + "givenName": { + "en": [ + "Smith" + ] + }, "hasClaim": [ { "id": "urn:epass:learningAchievement:2", @@ -56,7 +67,13 @@ } } ], - "id": "did:key:afsdlkj34134" + "id": "did:key:afsdlkj34134", + "identifier": { + "id": "urn:epass:identifier:2", + "notation": "545465468", + "schemeName": "Student ID", + "type": "Identifier" + } }, "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", "issuer": { diff --git a/json/mapping/custom_mapping_ELM_OBv3_latest.json b/json/mapping/custom_mapping_ELM_OBv3_latest.json index 2725cfd..fac6075 100644 --- a/json/mapping/custom_mapping_ELM_OBv3_latest.json +++ b/json/mapping/custom_mapping_ELM_OBv3_latest.json @@ -224,6 +224,66 @@ "format": "OBv3", "path": "$.credentialSchema" } + }, + { + "type_": "copy", + "source": { + "format": "ELM", + "path": "$.credentialStatus" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialStatus" + } + }, + { + "type_": "addIdentifier", + "source": { + "format": "ELM", + "datatype": "ext:studentID", + "path": "$.credentialSubject.identifier[0].notation" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.identifier[0]" + } + }, + { + "type_": "addIdentifier", + "source": { + "format": "ELM", + "datatype": "ext:givenName", + "path": "$.credentialSubject.givenName" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.identifier[1]" + } + }, + { + "type_": "addIdentifier", + "source": { + "format": "ELM", + "datatype": "ext:familyName", + "path": "$.credentialSubject.familyName" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.identifier[2]" + } + }, + { + "type_": "addIdentifier", + "source": { + "format": "ELM", + "datatype": "ext:fullName", + "path": "$.credentialSubject.fullName" + }, + "destination": { + "format": "OBv3", + "path": "$.credentialSubject.identifier[3]" + } } + ] \ No newline at end of file diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index 08721ae..1b4d4f4 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -87,17 +87,6 @@ "path": "$.proof" } }, - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.credentialSchema" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSchema" - } - }, { "type_": "markdownToJson", "source": { @@ -141,5 +130,76 @@ "format": "ELM", "path": "$.credentialSubject.hasClaim[0].specifiedBy.title.en[0]" } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSchema" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSchema" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialStatus" + }, + "destination": { + "format": "ELM", + "path": "$.credentialStatus" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "Student ID", + "path": "$.credentialSubject.identifier[0]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.identifier" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "ext:givenName", + "path": "$.credentialSubject.identifier[1]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.givenName" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "ext:familyName", + "path": "$.credentialSubject.identifier[2]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.givenName" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "ext:fullName", + "path": "$.credentialSubject.identifier[3]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.fullName" + } } + ] \ No newline at end of file diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 7fa3cb3..2eb5df1 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -2,13 +2,14 @@ use crate::{ backend::{ jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, - transformations::{DataLocation, StringValue, Transformation}, + transformations::{DataLocation, DataTypeLocation, StringValue, Transformation}, }, state::{AppState, Mapping}, trace_dbg, }; use jsonpath_rust::JsonPathFinder; -use serde_json::{json, Value}; +use serde_json::{json, Map, Value}; +//use tracing_subscriber::fmt::format; use std::{ collections::HashMap, ops::{Deref, DerefMut}, @@ -249,9 +250,52 @@ impl Repository { } } - + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + + Transformation::AddIdentifier { + type_: transformation, + source: + DataTypeLocation { + format: source_format, + datatype: source_type, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + let mut leaf_node = construct_leaf_node(&pointer); + let identifier_function_result = values_to_identity(&source_type, source_value); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(identifier_function_result); + } merge(destination_credential, leaf_node); @@ -259,10 +303,64 @@ impl Repository { Some((destination_path, source_path)) } + Transformation::IdentifierToObject { + type_: transformation, + source: + DataTypeLocation { + format: source_format, + datatype: source_type, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + let identifier_function_result = identity_to_object(&source_type, source_value); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(identifier_function_result); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + + _ => todo!(), } } + + + + + + pub fn apply_transformations( &mut self, transformations: Vec, @@ -355,6 +453,80 @@ fn remove_key_recursive(current_json: &mut Value, keys: &[String]) -> bool { false } + +fn values_to_identity(identity_type: &str, identity_value: Value) -> Value { + + //Create a new identity object that is fit for puprose in OBv3 (so not to lose information) + + let mut new_object = Map::new(); + new_object.insert("type".to_string(), Value::String("IdentityObject".to_string())); + new_object.insert("identityHash".to_string(), identity_value); + new_object.insert("identityType".to_string(), Value::String(identity_type.to_string())); + new_object.insert("hashed".to_string(), Value::Bool(false)); + new_object.insert("salt".to_string(), Value::String("not-used".to_string())); + let _current_value = Value::Object(new_object); + _current_value +} + +fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { + + //inspect the identity object and re write it so it can be reused in ELM + + //we need to achieve the followin structures: + // "identifier": [ + // { + // "id": "urn:epass:identifier:2", + // "type": "Identifier", + // "notation": "75541452", + // "schemeName": "Student ID" + // } + // ], + + // and for example + // "givenName": { + // "en": ["David"] + // }, + + + if let Some(id_value)= identity_value.get("identityHash") { + if identity_type.eq(&"Student ID".to_string()){ + let mut new_object = Map::new(); + new_object.insert("id".to_string(), Value::String("urn:epass:identifier:2".to_string())); + new_object.insert("type".to_string(), Value::String("Identifier".to_string())); + new_object.insert("notation".to_string(), id_value.clone()); + new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); + let _current_value = Value::Object(new_object); + _current_value + } + else { + id_value.clone() + } + } + else { + Value::String("".to_string()) + } + + + // if identity_type.eq(&"Student ID".to_string()){ + // let mut new_object = Map::new(); + // new_object.insert("id".to_string(), Value::String("urn:epass:identifier:2".to_string())); + // new_object.insert("type".to_string(), Value::String("Identifier".to_string())); + // new_object.insert("notation".to_string(), identity_value); + // new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); + // let _current_value = Value::Object(new_object); + // _current_value + // } + // else { + // if let Some(id_value)= identity_value.get("identityHash") { + // id_value.clone() + // } + // else { + // Value::String("".to_string()) + // } + // } +} + + fn json_to_markdown(json: &Value, indent_level: usize) -> String { let mut markdown = String::new(); let indent = " ".repeat(indent_level); @@ -389,9 +561,6 @@ fn json_to_markdown(json: &Value, indent_level: usize) -> String { markdown } - - - /// Recursively converts indented lines of Markdown into a JSON structure. fn markdown_to_json(lines: &[&str]) -> Value { let mut i = 0; diff --git a/src/backend/transformations.rs b/src/backend/transformations.rs index c7831b4..4a474f6 100644 --- a/src/backend/transformations.rs +++ b/src/backend/transformations.rs @@ -137,6 +137,37 @@ impl MarkdownToJson { } } +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum AddIdentifier { + addIdentifier, +} + +impl AddIdentifier { + pub fn apply(&self, value: Value) -> Value { + match self { + AddIdentifier::addIdentifier => value, + } + } +} + + + +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum IdentifierToObject { + identifierToObject, +} + +impl IdentifierToObject { + pub fn apply(&self, value: Value) -> Value { + match self { + IdentifierToObject::identifierToObject => value, + } + } +} + + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum Transformation { @@ -160,6 +191,16 @@ pub enum Transformation { source: DataLocation, destination: DataLocation, }, + AddIdentifier { + type_: AddIdentifier, + source: DataTypeLocation, + destination: DataLocation, + }, + IdentifierToObject { + type_: IdentifierToObject, + source: DataTypeLocation, + destination: DataLocation, + }, OneToMany { type_: OneToMany, source: DataLocation, @@ -178,6 +219,14 @@ pub struct DataLocation { pub path: String, } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DataTypeLocation { + pub format: String, + pub datatype: String, + pub path: String, +} + + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct StringValue { pub value: String, From f7c9047d5ef815929f47ad4b0ac6186f0dc68c35 Mon Sep 17 00:00:00 2001 From: hamrt Date: Fri, 11 Oct 2024 12:48:10 +0200 Subject: [PATCH 20/45] remove output from tool --- example_ELM-to-OBv3.json | 47 --------------------- example_ELM_to_OBv3.json | 91 ---------------------------------------- example_OBv3_to_ELM.json | 90 --------------------------------------- example_ebsi-obv3.json | 49 ---------------------- 4 files changed, 277 deletions(-) delete mode 100644 example_ELM-to-OBv3.json delete mode 100644 example_ELM_to_OBv3.json delete mode 100644 example_OBv3_to_ELM.json delete mode 100644 example_ebsi-obv3.json diff --git a/example_ELM-to-OBv3.json b/example_ELM-to-OBv3.json deleted file mode 100644 index 6f86dd2..0000000 --- a/example_ELM-to-OBv3.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "http://data.europa.eu/snb/model/context/edc-ap" - ], - "credentialSchema": [ - { - "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", - "type": "ShaclValidator2017" - }, - { - "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", - "type": "JsonSchema" - } - ], - "credentialSubject": { - "achievement": { - "criteria": { - "narrative": "**id**:\n urn:epass:learningOutcome:2\n**relatedSkill**:\n - **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo\n **inScheme**:\n **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/25831c2\n **type**:\n ConceptScheme\n **prefLabel**:\n **en**:\n - 5.4 Identifying digital competence gaps\n **type**:\n Concept\n**title**:\n **en**:\n - Name of DigiComp Competence\n**type**:\n LearningOutcome\n" - }, - "id": "urn:epass:learningAchievement:2", - "name": "Title of Achievement", - "type": [ - "Achievement" - ] - }, - "id": "did:key:afsdlkj34134", - "type": [ - "AchievementSubject" - ] - }, - "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", - "issuer": { - "id": "did:ebsi:org:12345689", - "name": "ORGANIZACION TEST", - "type": [ - "Profile" - ] - }, - "name": "TITLE OF PROGRAMME", - "type": [ - "VerifiableCredential", - "VerifiableAttestation", - "EuropeanDigitalCredential" - ], - "validFrom": "2019-09-20T00:00:00+02:00" -} \ No newline at end of file diff --git a/example_ELM_to_OBv3.json b/example_ELM_to_OBv3.json deleted file mode 100644 index 7cbd3bd..0000000 --- a/example_ELM_to_OBv3.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" - ], - "credentialSchema": [ - { - "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", - "type": "1EdTechJsonSchemaValidator2019" - }, - { - "id": "https://accrediter.edu/schema/endorsementcredential.json", - "type": "1EdTechJsonSchemaValidator2019" - } - ], - "credentialSubject": { - "achievement": { - "criteria": { - "narrative": "**id**:\n urn:epass:learningOutcome:2\n**relatedSkill**:\n - **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo\n **inScheme**:\n **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/25831c2\n **type**:\n ConceptScheme\n **prefLabel**:\n **en**:\n - 5.4 Identifying digital competence gaps\n **type**:\n Concept\n**title**:\n **en**:\n - Name of DigiComp Competence\n**type**:\n LearningOutcome\n" - }, - "id": "urn:epass:learningAchievement:2", - "name": "Title of Achievement", - "type": [ - "Achievement" - ] - }, - "id": "did:key:afsdlkj34134", - "identifier": [ - { - "hashed": false, - "identityHash": "545465468", - "identityType": "ext:studentID", - "salt": "not-used", - "type": "IdentityObject" - }, - { - "hashed": false, - "identityHash": { - "en": [ - "David" - ] - }, - "identityType": "ext:givenName", - "salt": "not-used", - "type": "IdentityObject" - }, - { - "hashed": false, - "identityHash": { - "en": [ - "Smith" - ] - }, - "identityType": "ext:familyName", - "salt": "not-used", - "type": "IdentityObject" - }, - { - "hashed": false, - "identityHash": { - "en": [ - "David Smith" - ] - }, - "identityType": "ext:fullName", - "salt": "not-used", - "type": "IdentityObject" - } - ], - "type": [ - "AchievementSubject" - ] - }, - "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", - "issuer": { - "address": { - "addressCountryCode": "http://publications.europa.eu/resource/authority/country/ESP" - }, - "id": "did:ebsi:org:12345689", - "name": "ORGANIZACION TEST", - "type": [ - "Profile" - ] - }, - "name": "TITLE OF PROGRAMME", - "type": [ - "VerifiableCredential", - "OpenBadgeCredential" - ], - "validFrom": "2019-09-20T00:00:00+02:00" -} \ No newline at end of file diff --git a/example_OBv3_to_ELM.json b/example_OBv3_to_ELM.json deleted file mode 100644 index d380b9b..0000000 --- a/example_OBv3_to_ELM.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "http://data.europa.eu/snb/model/context/edc-ap" - ], - "credentialSchema": [ - { - "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", - "type": "ShaclValidator2017" - }, - { - "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", - "type": "JsonSchema" - } - ], - "credentialSubject": { - "fullName": { - "en": [ - "David Smith" - ] - }, - "givenName": { - "en": [ - "Smith" - ] - }, - "hasClaim": [ - { - "id": "urn:epass:learningAchievement:2", - "specifiedBy": { - "learningOutcome": [ - { - "id": "urn:epass:learningOutcome:2", - "relatedSkill": [ - { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [ - "5.4 Identifying digital competence gaps" - ] - }, - "type": "Concept" - } - ], - "title": { - "en": [ - "Name of DigiComp Competence" - ] - }, - "type": "LearningOutcome" - } - ], - "title": { - "en": [ - "Title of Achievement" - ] - } - }, - "title": { - "en": [ - "TITLE OF PROGRAMME" - ] - } - } - ], - "id": "did:key:afsdlkj34134", - "identifier": { - "id": "urn:epass:identifier:2", - "notation": "545465468", - "schemeName": "Student ID", - "type": "Identifier" - } - }, - "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", - "issuer": { - "id": "did:ebsi:org:12345689", - "legalName": { - "en": "ORGANIZACION TEST" - } - }, - "type": [ - "VerifiableCredential", - "OpenBadgeCredential" - ], - "validFrom": "2019-09-20T00:00:00+02:00" -} \ No newline at end of file diff --git a/example_ebsi-obv3.json b/example_ebsi-obv3.json deleted file mode 100644 index 2bd1914..0000000 --- a/example_ebsi-obv3.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "@context": [ - "https://www.w3.org/ns/credentials/v2", - "http://data.europa.eu/snb/model/context/edc-ap" - ], - "credentialSchema": [ - { - "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", - "type": "ShaclValidator2017" - }, - { - "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", - "type": "JsonSchema" - } - ], - "credentialSubject": { - "achievement": { - "criteria": { - "narrative": "**id**:\n urn:epass:learningOutcome:2\n**relatedSkill**:\n - **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo\n **inScheme**:\n **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/25831c2\n **type**:\n ConceptScheme\n **prefLabel**:\n **en**:\n - 5.4 Identifying digital competence gaps\n **type**:\n Concept\n**title**:\n **en**:\n - Name of DigiComp Competence\n**type**:\n LearningOutcome\n" - }, - "id": "urn:epass:learningAchievement:2", - "name": "Title of Achievement", - "type": [ - "Achievement" - ] - }, - "id": "did:key:afsdlkj34134", - "type": [ - "AchievementSubject" - ] - }, - "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", - "issuer": { - "address": { - "addressCountryCode": "http://publications.europa.eu/resource/authority/country/ESP" - }, - "id": "did:ebsi:org:12345689", - "name": "ORGANIZACION TEST", - "type": [ - "Profile" - ] - }, - "name": "TITLE OF PROGRAMME", - "type": [ - "VerifiableCredential", - "OpenBadgeCredential" - ], - "validFrom": "2019-09-20T00:00:00+02:00" -} \ No newline at end of file From 224c01a7461ae41b5626c2790903deb92eaaca6d Mon Sep 17 00:00:00 2001 From: hamrt Date: Mon, 14 Oct 2024 18:14:29 +0200 Subject: [PATCH 21/45] add function to place array placeholders update testig --- .../custom_mapping_ELM_OBv3_latest.json | 27 ++++---- json/output/example_ELM_to_OBv3.json | 59 +++++++++++++++++ json/output/example_OBv3_to_ELM.json | 64 +++++++++++++++++++ json/output/readme.md | 11 ++++ src/backend/repository.rs | 54 ++++++++++------ src/backend/transformations.rs | 27 ++++++++ 6 files changed, 207 insertions(+), 35 deletions(-) create mode 100644 json/output/example_ELM_to_OBv3.json create mode 100644 json/output/example_OBv3_to_ELM.json create mode 100644 json/output/readme.md diff --git a/json/mapping/custom_mapping_ELM_OBv3_latest.json b/json/mapping/custom_mapping_ELM_OBv3_latest.json index fac6075..91b8bfe 100644 --- a/json/mapping/custom_mapping_ELM_OBv3_latest.json +++ b/json/mapping/custom_mapping_ELM_OBv3_latest.json @@ -172,13 +172,13 @@ } }, { - "type_": "stringit", + "type_": "stringArrayIt", "source": { - "value": "AchievementSubject" + "value": ["AchievementSubject", "profile"] }, "destination": { "format": "OBv3", - "path": "$.credentialSubject.type[0]" + "path": "$.credentialSubject.type" } }, { @@ -249,39 +249,36 @@ } }, { - "type_": "addIdentifier", + "type_": "copy", "source": { "format": "ELM", - "datatype": "ext:givenName", - "path": "$.credentialSubject.givenName" + "path": "$.credentialSubject.givenName.en[0]" }, "destination": { "format": "OBv3", - "path": "$.credentialSubject.identifier[1]" + "path": "$.credentialSubject.givenName" } }, { - "type_": "addIdentifier", + "type_": "copy", "source": { "format": "ELM", - "datatype": "ext:familyName", - "path": "$.credentialSubject.familyName" + "path": "$.credentialSubject.familyName.en[0]" }, "destination": { "format": "OBv3", - "path": "$.credentialSubject.identifier[2]" + "path": "$.credentialSubject.familyName" } }, { - "type_": "addIdentifier", + "type_": "copy", "source": { "format": "ELM", - "datatype": "ext:fullName", - "path": "$.credentialSubject.fullName" + "path": "$.credentialSubject.fullName.en[0]" }, "destination": { "format": "OBv3", - "path": "$.credentialSubject.identifier[3]" + "path": "$.credentialSubject.fullName" } } diff --git a/json/output/example_ELM_to_OBv3.json b/json/output/example_ELM_to_OBv3.json new file mode 100644 index 0000000..b20d4a2 --- /dev/null +++ b/json/output/example_ELM_to_OBv3.json @@ -0,0 +1,59 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://accrediter.edu/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialSubject": { + "achievement": { + "id": "urn:epass:learningAchievement:6", + "name": "German International Abitur", + "type": [ + "Achievement" + ] + }, + "familyName": "Smith", + "fullName": "David Smith", + "givenName": "David", + "id": "did:key:afsdlkj34134", + "identifier": [ + { + "hashed": false, + "identityHash": "5547554", + "identityType": "ext:studentID", + "salt": "not-used", + "type": "IdentityObject" + } + ], + "type": [ + "AchievementSubject", + "profile" + ] + }, + "id": "urn:credential:87ddce4d-de74-4838-b7a4-1f14d9c614ec", + "issuer": { + "address": { + "addressCountryCode": "http://publications.europa.eu/resource/authority/country/ESP" + }, + "id": "did:ebsi:org:12345689", + "name": "ORGANIZACION TEST", + "type": [ + "Profile" + ] + }, + "name": "Higher Education Entrance Qualification - German International ABITUR School", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "validFrom": "2020-07-20T00:00:00+02:00" +} \ No newline at end of file diff --git a/json/output/example_OBv3_to_ELM.json b/json/output/example_OBv3_to_ELM.json new file mode 100644 index 0000000..c6fbf71 --- /dev/null +++ b/json/output/example_OBv3_to_ELM.json @@ -0,0 +1,64 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "http://data.europa.eu/snb/model/context/edc-ap" + ], + "credentialSchema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialSubject": { + "fullName": { + "en": [ + "David Smith" + ] + }, + "givenName": { + "en": [ + "Smith" + ] + }, + "hasClaim": [ + { + "id": "urn:epass:learningAchievement:6", + "specifiedBy": { + "title": { + "en": [ + "German International Abitur" + ] + } + }, + "title": { + "en": [ + "Higher Education Entrance Qualification - German International ABITUR School" + ] + } + } + ], + "id": "did:key:afsdlkj34134", + "identifier": { + "id": "urn:epass:identifier:2", + "notation": "5547554", + "schemeName": "Student ID", + "type": "Identifier" + } + }, + "id": "urn:credential:87ddce4d-de74-4838-b7a4-1f14d9c614ec", + "issuer": { + "id": "did:ebsi:org:12345689", + "legalName": { + "en": "ORGANIZACION TEST" + } + }, + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "validFrom": "2020-07-20T00:00:00+02:00" +} \ No newline at end of file diff --git a/json/output/readme.md b/json/output/readme.md new file mode 100644 index 0000000..0c11ac0 --- /dev/null +++ b/json/output/readme.md @@ -0,0 +1,11 @@ +# ELM - OpenBadges v3 testing + +## Overview +To do a quick test of the translation run from the credential-converter directory + +cargo run -- -i ./json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic.json -o ./json/output/example_ELM_to_OBv3.json -m ./json/mapping/custom_mapping_ELM_OBv3_latest.json -c ELMtoOBv3 + +cargo run -- -i ./json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/Bengales_highSchoolDiploma.json -o ./json/output/example_ELM_to_OBv3.json -m ./json/mapping/custom_mapping_ELM_OBv3_latest.json -c ELMtoOBv3 + + +cargo run -- -i ./json/output/example_ELM_to_OBv3.json -o ./json/output/example_OBv3_to_ELM.json -m ./json/mapping/custom_mapping_OBv3_ELM_latest.json -c OBv3toELM \ No newline at end of file diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 2eb5df1..45c17c4 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -2,7 +2,7 @@ use crate::{ backend::{ jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, - transformations::{DataLocation, DataTypeLocation, StringValue, Transformation}, + transformations::{DataLocation, DataTypeLocation, StringValue, StringArrayValue, Transformation}, }, state::{AppState, Mapping}, trace_dbg, @@ -147,7 +147,7 @@ impl Repository { let pointer = JsonPointer::try_from(JsonPath(destination_path)).unwrap(); let mut leaf_node = construct_leaf_node(&pointer); - if let Some(value) = leaf_node.pointer_mut(&pointer) { + if let Some(value) = leaf_node.pointer_mut(&pointer) { *value = transformation.apply(source_value); } @@ -155,6 +155,38 @@ impl Repository { None } + Transformation::StringArrayToOne { + type_: transformation, + source: StringArrayValue { value: source_value }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if destination_format != mapping.output_format() { + return None; + } + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path)).unwrap(); + let mut leaf_node = construct_leaf_node(&pointer); + if let Some(value) = leaf_node.pointer_mut(&pointer) { + let json_value: Value = Value::Array( + source_value.into_iter() + .map(Value::String) // Convert each String into serde_json::Value::String + .collect() + ); + *value = transformation.apply(json_value); + } + + merge(destination_credential, leaf_node); + None + } + + + + Transformation::JsonToMarkdown { type_: transformation, source: @@ -506,24 +538,6 @@ fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { Value::String("".to_string()) } - - // if identity_type.eq(&"Student ID".to_string()){ - // let mut new_object = Map::new(); - // new_object.insert("id".to_string(), Value::String("urn:epass:identifier:2".to_string())); - // new_object.insert("type".to_string(), Value::String("Identifier".to_string())); - // new_object.insert("notation".to_string(), identity_value); - // new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); - // let _current_value = Value::Object(new_object); - // _current_value - // } - // else { - // if let Some(id_value)= identity_value.get("identityHash") { - // id_value.clone() - // } - // else { - // Value::String("".to_string()) - // } - // } } diff --git a/src/backend/transformations.rs b/src/backend/transformations.rs index 4a474f6..705b4de 100644 --- a/src/backend/transformations.rs +++ b/src/backend/transformations.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; + #[allow(non_camel_case_types)] #[derive(Serialize, Deserialize, Debug, Clone)] pub enum OneToOne { @@ -109,6 +110,21 @@ impl StringToOne { } } + +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum StringArrayToOne { + stringArrayIt, +} + +impl StringArrayToOne { + pub fn apply(&self, value: Value) -> Value { + match self { + StringArrayToOne::stringArrayIt => value, + } + } +} + #[allow(non_camel_case_types)] #[derive(Serialize, Deserialize, Debug, Clone)] pub enum JsonToMarkdown { @@ -181,6 +197,11 @@ pub enum Transformation { source: StringValue, destination: DataLocation, }, + StringArrayToOne { + type_: StringArrayToOne, + source: StringArrayValue, + destination: DataLocation, + }, MarkdownToJson { type_: MarkdownToJson, source: DataLocation, @@ -231,3 +252,9 @@ pub struct DataTypeLocation { pub struct StringValue { pub value: String, } + + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct StringArrayValue { + pub value: Vec, +} From f840f24e9258a1e3ddd114f5e55f98de542de546 Mon Sep 17 00:00:00 2001 From: hamrt Date: Sun, 20 Oct 2024 16:54:37 +0200 Subject: [PATCH 22/45] added webservice feature to allow for usage though a website --- Cargo.lock | 239 +++++++++++++++++++++++++-- Cargo.toml | 3 + README.md | 14 ++ json/output/example_OBv3_to_ELM.json | 10 -- src/backend/headless_cli.rs | 5 +- src/backend/init_conversion.rs | 5 +- src/backend/mod.rs | 2 + src/backend/repository.rs | 67 +++----- src/backend/routes/mod.rs | 17 ++ src/backend/routes/root.rs | 41 +++++ src/backend/routes/translate_file.rs | 151 +++++++++++++++++ src/backend/transformations.rs | 9 +- src/backend/web.rs | 59 +++++++ src/main.rs | 21 ++- 14 files changed, 560 insertions(+), 83 deletions(-) create mode 100644 src/backend/routes/mod.rs create mode 100644 src/backend/routes/root.rs create mode 100644 src/backend/routes/translate_file.rs create mode 100644 src/backend/web.rs diff --git a/Cargo.lock b/Cargo.lock index a506994..c2a5982 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,6 +139,74 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "axum" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +dependencies = [ + "async-trait", + "axum-core", + "axum-macros", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -229,9 +297,9 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "bytes" -version = "1.6.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "cassowary" @@ -440,6 +508,7 @@ name = "credential-converter" version = "0.1.0" dependencies = [ "anyhow", + "axum", "clap", "color-eyre", "config", @@ -447,6 +516,7 @@ dependencies = [ "csv", "digital-credential-data-models", "directories", + "eyre", "jsonpath-rust", "jsonschema", "lazy_static", @@ -460,6 +530,7 @@ dependencies = [ "serde_path_to_error", "strum", "tokio", + "tower-http", "tracing", "tracing-error", "tracing-subscriber", @@ -815,7 +886,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", "indexmap 2.2.6", "slab", "tokio", @@ -868,6 +939,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -875,7 +957,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -902,8 +1007,8 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -915,6 +1020,41 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.5.0", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -1173,6 +1313,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.7.4" @@ -1212,6 +1358,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.1.0", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + [[package]] name = "nom" version = "7.1.3" @@ -1647,9 +1810,9 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.29", "ipnet", "js-sys", "log", @@ -1660,7 +1823,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tower-service", @@ -1936,6 +2099,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stability" version = "0.2.0" @@ -2016,6 +2185,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + [[package]] name = "system-configuration" version = "0.5.1" @@ -2225,11 +2400,50 @@ dependencies = [ "winnow 0.6.13", ] +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -2237,6 +2451,7 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", diff --git a/Cargo.toml b/Cargo.toml index 3016520..a93849e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,6 @@ jsonpath-rust = "0.5" strum = "0.26.2" digital-credential-data-models = { git = "https://github.com/impierce/digital-credential-data-models.git", rev = "9f16c27" } csv = "1.3.0" +axum = { version = "0.7.7", features = ["macros", "multipart"] } +eyre = "0.6.8" +tower-http = { version = "0.6.1", features = ["limit", "trace"] } diff --git a/README.md b/README.md index 8e8c633..c6b09fa 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,20 @@ Or find the executable in the `/target/debug` folder named after the repo name ` ./target/debug/credential-converter -i example.json -o example_123.json -m example_mapping.json -c o-bv3-to-elm ``` +For headless/webservice execution: + +Run with -w +The default address and port for the application are 127.0.0.1:3000 but you can also specify the address and port yourself + +```sh +cargo run -- -w +``` +example for running on 192.168.1.1:5000 +```sh +cargo run -- -w 192.168.1.1:5000 +``` + +a webpage displaying a form can be found at the root of the project a webservice api can be found at /translate_file *Warning: the ratatui library does not seem to handle different color settings in your terminal perfectly. This causes the colors to differ slightly between builds in different terminals. For reference please continue reading the readme, colors will be explained accompanied by screenshots.* diff --git a/json/output/example_OBv3_to_ELM.json b/json/output/example_OBv3_to_ELM.json index c6fbf71..9bcbe97 100644 --- a/json/output/example_OBv3_to_ELM.json +++ b/json/output/example_OBv3_to_ELM.json @@ -14,16 +14,6 @@ } ], "credentialSubject": { - "fullName": { - "en": [ - "David Smith" - ] - }, - "givenName": { - "en": [ - "Smith" - ] - }, "hasClaim": [ { "id": "urn:epass:learningAchievement:6", diff --git a/src/backend/headless_cli.rs b/src/backend/headless_cli.rs index 478255b..9cc7128 100644 --- a/src/backend/headless_cli.rs +++ b/src/backend/headless_cli.rs @@ -1,5 +1,5 @@ -use crate::backend::init_conversion::load_mapping_file; use crate::backend::init_conversion::init_conversion; +use crate::backend::init_conversion::load_mapping_file; use crate::p2_p3_common::create_output_files; use crate::state::{AppState, Mapping}; use crate::trace_dbg; @@ -149,6 +149,9 @@ pub struct Args { #[arg(short, long, value_enum, required_if_eq_any = [("mapping_file", "Some"), ("input_file", "Some"), ("input_directory", "Some"), ("output_file", "Some"), ("output_directory", "Some")])] conversion: Option, + + #[arg(short, long)] + web_service: Option>, // #[arg(short, long)] // todo: nice feature for in the future // prefix_output: String, diff --git a/src/backend/init_conversion.rs b/src/backend/init_conversion.rs index 76f4863..765f293 100644 --- a/src/backend/init_conversion.rs +++ b/src/backend/init_conversion.rs @@ -95,7 +95,7 @@ fn enter_fixed_context_values(state: &mut AppState) { Value::Array(vec![ json!("https://www.w3.org/ns/credentials/v2"), json!("http://data.europa.eu/snb/model/context/edc-ap"), - ]), + ]), ); } else if state.mapping.output_format() == "OBv3" { let output_obv3 = state.repository.get_mut("OBv3").unwrap().as_object_mut().unwrap(); @@ -107,10 +107,8 @@ fn enter_fixed_context_values(state: &mut AppState) { ]), ); } - } - /// Enter fixed values into '@context' field, as demanded by the respective json-schema fn enter_fixed_schema_values(state: &mut AppState) { if state.mapping.output_format() == "ELM" { @@ -133,7 +131,6 @@ fn enter_fixed_schema_values(state: &mut AppState) { } } - //////// HELPERS //////// pub fn get_json(path: impl AsRef) -> Result diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 6814f84..e09ddcc 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -7,5 +7,7 @@ pub mod jsonpointer; pub mod leaf_nodes; pub mod logging; pub mod repository; +pub mod routes; pub mod transformations; pub mod update_display; +pub mod web; diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 45c17c4..59d8aac 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -2,7 +2,7 @@ use crate::{ backend::{ jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, - transformations::{DataLocation, DataTypeLocation, StringValue, StringArrayValue, Transformation}, + transformations::{DataLocation, DataTypeLocation, StringArrayValue, StringValue, Transformation}, }, state::{AppState, Mapping}, trace_dbg, @@ -147,7 +147,7 @@ impl Repository { let pointer = JsonPointer::try_from(JsonPath(destination_path)).unwrap(); let mut leaf_node = construct_leaf_node(&pointer); - if let Some(value) = leaf_node.pointer_mut(&pointer) { + if let Some(value) = leaf_node.pointer_mut(&pointer) { *value = transformation.apply(source_value); } @@ -173,9 +173,10 @@ impl Repository { let mut leaf_node = construct_leaf_node(&pointer); if let Some(value) = leaf_node.pointer_mut(&pointer) { let json_value: Value = Value::Array( - source_value.into_iter() - .map(Value::String) // Convert each String into serde_json::Value::String - .collect() + source_value + .into_iter() + .map(Value::String) // Convert each String into serde_json::Value::String + .collect(), ); *value = transformation.apply(json_value); } @@ -184,9 +185,6 @@ impl Repository { None } - - - Transformation::JsonToMarkdown { type_: transformation, source: @@ -268,7 +266,6 @@ impl Repository { let mut leaf_node = construct_leaf_node(&pointer); - if let Some(inner_string) = &source_value.as_str() { let mut lines: Vec<&str> = inner_string.lines().collect(); @@ -276,7 +273,7 @@ impl Repository { // Split the string by newlines and collect into Vec<&str> let markdown_function_result = markdown_to_json(&lines); - + if let Some(value) = leaf_node.pointer_mut(&pointer) { *value = transformation.apply(markdown_function_result); } @@ -288,13 +285,12 @@ impl Repository { Some((destination_path, source_path)) } - Transformation::AddIdentifier { type_: transformation, source: DataTypeLocation { format: source_format, - datatype: source_type, + datatype: source_type, path: source_path, }, destination: @@ -318,13 +314,13 @@ impl Repository { return None; } }; - + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); let mut leaf_node = construct_leaf_node(&pointer); let identifier_function_result = values_to_identity(&source_type, source_value); - + if let Some(value) = leaf_node.pointer_mut(&pointer) { *value = transformation.apply(identifier_function_result); } @@ -340,7 +336,7 @@ impl Repository { source: DataTypeLocation { format: source_format, - datatype: source_type, + datatype: source_type, path: source_path, }, destination: @@ -364,13 +360,13 @@ impl Repository { return None; } }; - + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); let mut leaf_node = construct_leaf_node(&pointer); let identifier_function_result = identity_to_object(&source_type, source_value); - + if let Some(value) = leaf_node.pointer_mut(&pointer) { *value = transformation.apply(identifier_function_result); } @@ -381,18 +377,10 @@ impl Repository { Some((destination_path, source_path)) } - - _ => todo!(), } } - - - - - - pub fn apply_transformations( &mut self, transformations: Vec, @@ -485,9 +473,7 @@ fn remove_key_recursive(current_json: &mut Value, keys: &[String]) -> bool { false } - fn values_to_identity(identity_type: &str, identity_value: Value) -> Value { - //Create a new identity object that is fit for puprose in OBv3 (so not to lose information) let mut new_object = Map::new(); @@ -501,9 +487,8 @@ fn values_to_identity(identity_type: &str, identity_value: Value) -> Value { } fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { - //inspect the identity object and re write it so it can be reused in ELM - + //we need to achieve the followin structures: // "identifier": [ // { @@ -519,9 +504,8 @@ fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { // "en": ["David"] // }, - - if let Some(id_value)= identity_value.get("identityHash") { - if identity_type.eq(&"Student ID".to_string()){ + if let Some(id_value) = identity_value.get("identityHash") { + if identity_type.eq(&"Student ID".to_string()) { let mut new_object = Map::new(); new_object.insert("id".to_string(), Value::String("urn:epass:identifier:2".to_string())); new_object.insert("type".to_string(), Value::String("Identifier".to_string())); @@ -529,18 +513,14 @@ fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); let _current_value = Value::Object(new_object); _current_value + } else { + id_value.clone() } - else { - id_value.clone() - } - } - else { + } else { Value::String("".to_string()) } - } - fn json_to_markdown(json: &Value, indent_level: usize) -> String { let mut markdown = String::new(); let indent = " ".repeat(indent_level); @@ -640,7 +620,7 @@ fn markdown_to_json(lines: &[&str]) -> Value { } // setup the vector array for the right value and position - if depth >= position.len() - 1 && i >0 { + if depth >= position.len() - 1 && i > 0 { //test previous line to see is we might have a nested object let (last_obj_type, _new_depth) = evaluate_line(lines[i - 1]); if last_obj_type == "O" { @@ -747,11 +727,7 @@ fn markdown_to_json(lines: &[&str]) -> Value { fn evaluate_line(line_to_test: &str) -> (String, usize) { //test depth - let mut depth = line_to_test - .chars() - .take_while(|c| c.is_whitespace()) - .count() - / 2; + let mut depth = line_to_test.chars().take_while(|c| c.is_whitespace()).count() / 2; //test type let line_type; let trimmed = line_to_test.trim(); @@ -787,7 +763,6 @@ fn cleanup_string(string_to_clean: &str) -> String { format!("\"{}\"", cleaned_string) } - // 1. Parsing Markdown: // • Headings (#): These are treated as keys in the resulting JSON object. // • Bold Text (**): This is also treated as a key in the JSON object. diff --git a/src/backend/routes/mod.rs b/src/backend/routes/mod.rs new file mode 100644 index 0000000..243f811 --- /dev/null +++ b/src/backend/routes/mod.rs @@ -0,0 +1,17 @@ +pub mod root; +pub mod translate_file; + +use axum::{extract::DefaultBodyLimit, routing::get, routing::post, Router}; +//use save_file::save_file; +use root::root; +use translate_file::translate_file; + +pub fn create_router() -> Router { + Router::new() + .route( + "/translate_file", + post(translate_file).route_layer(DefaultBodyLimit::max(135476000)), + ) + .route("/", get(root)) + // .layer(tower_http::trace::TraceLayer::new_for_http()) +} diff --git a/src/backend/routes/root.rs b/src/backend/routes/root.rs new file mode 100644 index 0000000..5d9c3d9 --- /dev/null +++ b/src/backend/routes/root.rs @@ -0,0 +1,41 @@ +use axum::{ + response::Html, + // routing::post, + // Router, +}; + +pub async fn root() -> Html<&'static str> { + Html( + r#" + + + Open Badges to ELM converter + +

Open Badges to ELM converter

+

+ This from is part of a service to translate Open Badges to ELM European Digital Credentials.
+ The code as two parts a webservice that is called through this form and a CLI.
+ The CLI can be found at the Educredentials github respository +

+
+ +

+ +

+ +

+ + + "#, + ) +} diff --git a/src/backend/routes/translate_file.rs b/src/backend/routes/translate_file.rs new file mode 100644 index 0000000..2138161 --- /dev/null +++ b/src/backend/routes/translate_file.rs @@ -0,0 +1,151 @@ +use axum::extract::multipart::Field; +use axum::{ + extract::Multipart, + // routing::post, + // Router, + http::{header, HeaderMap, HeaderValue, StatusCode}, + response::{IntoResponse, Response}, +}; + +use crate::backend::headless_cli::load_files_apply_transformations; +use crate::state::AppState; +use std::{fs::File, io::Write, path::Path}; +use tokio::fs; + +pub async fn translate_file(mut multipart: Multipart) -> Result { + // Create directories for uploads and outputs if they don't exist + let upload_dir = "uploads"; + let output_dir = "outputs"; + fs::create_dir_all(upload_dir).await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to create upload directory".to_string(), + ) + })?; + fs::create_dir_all(output_dir).await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to create output directory".to_string(), + ) + })?; + + // Handle the file upload + let mut input_file_path = String::new(); + let mut mapping_file_name = String::new(); + + while let Some(field) = multipart + .next_field() + .await + .map_err(|_| (StatusCode::BAD_REQUEST, "Failed to process uploaded file".to_string()))? + { + let name = field.name().unwrap().to_string(); + match name.as_str() { + "input_file" => match process_file_field(&field) { + Ok(file_name) => { + input_file_path = format!("{}/{}", upload_dir, file_name); + let mut file = File::create(&input_file_path) + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to create file".to_string()))?; + let data = field.bytes().await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to read file data".to_string(), + ) + })?; + file.write_all(&data) + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to write to file".to_string()))?; + } + Err(e) => eprintln!("Error: {}", e), + }, + "translation" => { + // Mapping::OBv3ToELM => "OBv3".to_string(), + // Mapping::ELMToOBv3 => "ELM".to_string(), + if let Ok(translation_value) = field.text().await { + // Match on the value of the text field and call different functions + match translation_value.as_str() { + "OBv3ToELM" => { + mapping_file_name = format!("json/mapping/custom_mapping_OBv3_ELM_latest.json"); + } + "ELMToOBv3" => { + mapping_file_name = format!("json/mapping/custom_mapping_ELM_OBv3_latest.json"); + } + _ => { + return Err(( + StatusCode::BAD_REQUEST, + format!("Invalid translation value: {}", translation_value), + )) + } + }; + } else { + // Handle the case where reading the field text fails + return Err((StatusCode::BAD_REQUEST, "Failed to read translation value".to_string())); + } + } + &_ => return Err((StatusCode::BAD_REQUEST, "Received unwanted values".to_string())), + } + } + + // Define the output file path + let output_file_name = format!( + "translated_{}", + Path::new(&input_file_path).file_name().unwrap().to_str().unwrap() + ); + let output_file_path = format!("{}/{}", output_dir, output_file_name); + + // start mapping based on the input form the API + // 1 create a state needed for the mapping tool + // 2 load all hte state elements needed for mapping + + let mut state = AppState::default(); + state.input_path = input_file_path; + state.output_path = output_file_path.clone(); + state.mapping_path = mapping_file_name; + + load_files_apply_transformations(&mut state); + + // Return the translated file as a response + // 1 load file from fs into mem + // 2 remove file from fs + // 3 push mem to http output + + let output_file = fs::read(&output_file_path).await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to read output file".to_string(), + ) + })?; + fs::remove_file(state.input_path).await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to remove output file".to_string(), + ) + })?; + fs::remove_file(state.output_path).await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to remove output file".to_string(), + ) + })?; + + // Set the headers, including content disposition for download + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_static("application/octet-stream"), + ); + headers.insert( + header::CONTENT_DISPOSITION, + HeaderValue::from_str(&format!("attachment; filename=\"{}\"", output_file_name)).unwrap(), + ); + + // Return the file content along with the appropriate headers + Ok((headers, output_file).into_response()) + + // Ok(output_file.into_response()) +} + +fn process_file_field(field: &Field) -> Result { + match field.file_name() { + Some(file_name) => Ok(file_name.to_string()), + None => Err("No file name found in the field.".to_string()), + } +} diff --git a/src/backend/transformations.rs b/src/backend/transformations.rs index 705b4de..57f854e 100644 --- a/src/backend/transformations.rs +++ b/src/backend/transformations.rs @@ -1,7 +1,6 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; - #[allow(non_camel_case_types)] #[derive(Serialize, Deserialize, Debug, Clone)] pub enum OneToOne { @@ -110,7 +109,6 @@ impl StringToOne { } } - #[allow(non_camel_case_types)] #[derive(Serialize, Deserialize, Debug, Clone)] pub enum StringArrayToOne { @@ -167,8 +165,6 @@ impl AddIdentifier { } } - - #[allow(non_camel_case_types)] #[derive(Serialize, Deserialize, Debug, Clone)] pub enum IdentifierToObject { @@ -183,7 +179,6 @@ impl IdentifierToObject { } } - #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum Transformation { @@ -243,17 +238,15 @@ pub struct DataLocation { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct DataTypeLocation { pub format: String, - pub datatype: String, + pub datatype: String, pub path: String, } - #[derive(Serialize, Deserialize, Debug, Clone)] pub struct StringValue { pub value: String, } - #[derive(Serialize, Deserialize, Debug, Clone)] pub struct StringArrayValue { pub value: Vec, diff --git a/src/backend/web.rs b/src/backend/web.rs new file mode 100644 index 0000000..cf5e298 --- /dev/null +++ b/src/backend/web.rs @@ -0,0 +1,59 @@ +use crate::backend::routes::create_router; + +use eyre::Result; + +use std::net::{IpAddr, SocketAddr}; +use std::str::FromStr; + +pub struct Server { + address: IpAddr, + port: u16, +} + +impl Server { + pub fn new(address: Option) -> Self { + // Use default address if no input is provided + let default_address = "127.0.0.1".to_string(); + let default_port = 3000; + + // Parse the provided address and port or fall back to defaults + let address = address.unwrap_or(default_address); + let mut split = address.split(':'); + let ip_part = split.next().unwrap_or("127.0.0.1"); + let port_part = split.next().unwrap_or("3000"); + + let address: IpAddr = IpAddr::from_str(ip_part).unwrap_or_else(|_| IpAddr::from([127, 0, 0, 1])); + let port: u16 = port_part.parse().unwrap_or(default_port); + + Self { address, port } + } + + pub async fn run(&self) -> Result<()> { + let app = create_router(); + let address = SocketAddr::from((self.address, self.port)); + + tracing::info!("server running on port: {}", self.port); + + // run our app with hyper, listening globally on port 3000 + let listener = tokio::net::TcpListener::bind(address).await.unwrap(); + axum::serve(listener, app).await.unwrap(); + + Ok(()) + } +} + +impl Default for Server { + fn default() -> Self { + Self::new(None) + } +} + +#[tokio::main] +pub async fn api_service(address: Option) { + let server = Server::new(address); + + match server.run().await { + Ok(_) => println!("Server exited"), + Err(error) => panic!("Server exited with error: {error}"), + } +} diff --git a/src/main.rs b/src/main.rs index c680df3..11e97b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ use crossterm::execute; use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}; use ratatui::prelude::{CrosstermBackend, Terminal}; use std::io::{stdout, Result}; +use std::net::SocketAddr; // Load I18n macro, for allow you use `t!` macro in anywhere. #[macro_use] @@ -29,11 +30,27 @@ fn main() -> Result<()> { let mut state = AppState::default(); + // if arguments are passed we are working headless + // two options: + // 1. headless CLI (with arguments -o -i -m) + // 2. headless webservice (with argument -w) if std::env::args().len() > 1 { trace_dbg!("Arguments detected, running headless conversion"); - let mut cli_args = Args::parse(); - run_headless(&mut cli_args, &mut state)?; + println!("Arguments detected, running headless conversion"); + let args: Vec = std::env::args().collect(); + if args.contains(&"-w".to_string()) { + println!("Let's run the webservice!!!"); + if args[args.len() - 1].parse::().is_ok() { + println!("Let's use {}", args[2].clone()); + crate::backend::web::api_service(Some(args[args.len() - 1].clone())); + } else { + println!("The webservice runs default 127.0.0.1:3000"); + crate::backend::web::api_service(None); + } + } else { + run_headless(&mut cli_args, &mut state)?; + } } else { trace_dbg!("No arguments detected, starting the TUI"); From d6b51ba4b8b97dbc00638c9491a0f8cb2d2e66eb Mon Sep 17 00:00:00 2001 From: hamrt Date: Mon, 21 Oct 2024 22:20:13 +0200 Subject: [PATCH 23/45] added light weight docker files to create easy image --- Dockerfile | 31 +++++++++++++++++++++++++++++++ docker-compose.yaml | 12 ++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3040142 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM rust:1.82 AS build + +# 1. create a workdir to start working from +WORKDIR /credential-converter +# 2. Copy our manifests +COPY ./Cargo.lock ./Cargo.lock +COPY ./Cargo.toml ./Cargo.toml + +# # 3. build depedencies for the project +# RUN cargo build --release + +# 4. copy over the origninal source and reference files +COPY ./src ./src +COPY ./json ./json + +# 5. build the project +RUN cargo build --release + + + +# our final base +FROM rust:1.82-slim + +# copy the build artifact from the build stage +COPY --from=build /credential-converter/target/release/credential-converter . +COPY --from=build /credential-converter/json ./json + +# set the startup command to run your binary +CMD ["./credential-converter", "-w", "0.0.0.0:3000"] + +EXPOSE 3000 diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..2269bd1 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,12 @@ +version: '3.7' + +services: + credential-converter: + container_name: credential-converter + image: credential-converter:latest + build: . + ports: + - 3000:3000 + expose: + - 3000 + From d02fcbbe4c384c21987c287d186dfbab4885e4c3 Mon Sep 17 00:00:00 2001 From: hamrt Date: Mon, 21 Oct 2024 22:24:59 +0200 Subject: [PATCH 24/45] imprvement to docker-compose --- docker-compose.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 2269bd1..0e8860a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: '3.7' - services: credential-converter: container_name: credential-converter From 38f84652d8a266ea362ed176a7cf11abe390435b Mon Sep 17 00:00:00 2001 From: hamrt Date: Mon, 4 Nov 2024 11:00:01 +0100 Subject: [PATCH 25/45] update to fix issue with parsing a string that is not markdown --- src/backend/repository.rs | 265 ++++++++++++++++++++------------------ 1 file changed, 140 insertions(+), 125 deletions(-) diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 59d8aac..50148c8 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -563,166 +563,181 @@ fn markdown_to_json(lines: &[&str]) -> Value { //lets create a string to which we will concatenate new lines based on the markdown lines position.insert(0, "".to_string()); let mut json_string = String::from(""); + // evaluate if the input contains markdown or not + // if markdown is detected we will try to create json otherwise the value of the "markdown" will be put straight into the attribute + if lines.contains(&"**") == false { + while i < lines.len() { + let line = lines[i]; + json_string.push_str(line); + i += 1; + } + let parsed_json: Value = serde_json::to_value(&json_string).unwrap(); + parsed_json - while i < lines.len() { - let line = lines[i]; + } else { - // Handle key-value pairs (e.g., **key**: value) - let (obj_type, depth) = evaluate_line(line); - if obj_type == "E" && i == 0 { - // open a json object - json_string.push_str("{\n"); - } - if obj_type == "O" { - while depth < position.len() - 1 { - // we need to close the positions - if let Some(last_value) = position.last() { - if last_value == "O" { - // The last value is O we can now close this object - if let Some(_last_value) = position.pop() { - json_string.pop(); - json_string = json_string.trim_end_matches(',').to_string(); - json_string.push_str("},\n"); - } - } else if last_value == "A" { - // close value A - if let Some(_last_value) = position.pop() { - json_string.push_str("]\n"); - } - } else if last_value == "OA" && depth < position.len() - 1 { - //The last value is OA - if let Some(_last_value) = position.pop() { - // first close the array - json_string.pop(); - json_string = json_string.trim_end_matches(',').to_string(); - json_string.push_str("],\n"); - if depth > 0 { - if let Some(_last_value) = position.pop() { - // then close the object - json_string.push_str("},\n"); + + while i < lines.len() { + let line = lines[i]; + + // Handle key-value pairs (e.g., **key**: value) + let (obj_type, depth) = evaluate_line(line); + if obj_type == "E" && i == 0 { + // open a json object + json_string.push_str("{\n"); + } + + if obj_type == "O" { + while depth < position.len() - 1 { + // we need to close the positions + if let Some(last_value) = position.last() { + if last_value == "O" { + // The last value is O we can now close this object + if let Some(_last_value) = position.pop() { + json_string.pop(); + json_string = json_string.trim_end_matches(',').to_string(); + json_string.push_str("},\n"); + } + } else if last_value == "A" { + // close value A + if let Some(_last_value) = position.pop() { + json_string.push_str("]\n"); + } + } else if last_value == "OA" && depth < position.len() - 1 { + //The last value is OA + if let Some(_last_value) = position.pop() { + // first close the array + json_string.pop(); + json_string = json_string.trim_end_matches(',').to_string(); + json_string.push_str("],\n"); + if depth > 0 { + if let Some(_last_value) = position.pop() { + // then close the object + json_string.push_str("},\n"); + } + } else { + //"The vector was empty, nothing to remove."); } } else { - //"The vector was empty, nothing to remove."); + // ("The vector was empty, nothing to remove."); } } else { - // ("The vector was empty, nothing to remove."); + // we have a different last value we will remove it from tha vector + if let Some(_last_value) = position.pop() { + // json_string.push_str("]\n"); + } } } else { - // we have a different last value we will remove it from tha vector - if let Some(_last_value) = position.pop() { - // json_string.push_str("]\n"); - } + // println!("The vector is empty."); } - } else { - // println!("The vector is empty."); } - } - // setup the vector array for the right value and position - if depth >= position.len() - 1 && i > 0 { - //test previous line to see is we might have a nested object - let (last_obj_type, _new_depth) = evaluate_line(lines[i - 1]); - if last_obj_type == "O" { - json_string.push('{'); - json_string.push_str(&cleanup_string(line)); - json_string.push(':'); - position.insert(depth, "O".to_string()); - } else if let Some(last_value) = position.last() { - if last_value == "OA" && depth == position.len() - 1 { - json_string.push_str(&cleanup_string(line)); - json_string.push(':'); - } else if depth > position.len() - 1 { + // setup the vector array for the right value and position + if depth >= position.len() - 1 && i > 0 { + //test previous line to see is we might have a nested object + let (last_obj_type, _new_depth) = evaluate_line(lines[i - 1]); + if last_obj_type == "O" { json_string.push('{'); json_string.push_str(&cleanup_string(line)); json_string.push(':'); position.insert(depth, "O".to_string()); - } else { - json_string.push_str(&cleanup_string(line)); - json_string.push(':'); - position[depth] = "O".to_string(); + } else if let Some(last_value) = position.last() { + if last_value == "OA" && depth == position.len() - 1 { + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + } else if depth > position.len() - 1 { + json_string.push('{'); + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + position.insert(depth, "O".to_string()); + } else { + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + position[depth] = "O".to_string(); + } } } - } - } else if obj_type == "A" { - while depth < position.len() - 1 { - // we need to close the positions - if let Some(last_value) = position.last() { - if last_value == "O" { - if let Some(_last_value) = position.pop() { - json_string.push_str("},\n"); - } - } else if last_value == "A" { - if let Some(_last_value) = position.pop() { - json_string.push_str("]\n"); + } else if obj_type == "A" { + while depth < position.len() - 1 { + // we need to close the positions + if let Some(last_value) = position.last() { + if last_value == "O" { + if let Some(_last_value) = position.pop() { + json_string.push_str("},\n"); + } + } else if last_value == "A" { + if let Some(_last_value) = position.pop() { + json_string.push_str("]\n"); + } + } else { + // The last value is something leave it } } else { - // The last value is something leave it + // The vector is empty. } - } else { - // The vector is empty. } - } - // setup the vector array for the right value and position - if depth >= position.len() - 1 { - if let Some(last_value) = position.last() { - if last_value == "OA" { - json_string.push('['); - } else if last_value == "A" && depth == position.len() - 1 { - position.insert(depth, "A".to_string()); - } else { - position.insert(depth, "A".to_string()); - json_string.push('['); + // setup the vector array for the right value and position + if depth >= position.len() - 1 { + if let Some(last_value) = position.last() { + if last_value == "OA" { + json_string.push('['); + } else if last_value == "A" && depth == position.len() - 1 { + position.insert(depth, "A".to_string()); + } else { + position.insert(depth, "A".to_string()); + json_string.push('['); + } } + json_string.push_str(&cleanup_string(line)); } - json_string.push_str(&cleanup_string(line)); - } - } else if obj_type == "OA" { - while depth < position.len() - 1 { - // we need to close the positions - if let Some(last_value) = position.last() { - if last_value == "O" { - if let Some(_last_value) = position.pop() { - json_string.pop(); - json_string.pop(); - json_string.push_str("},\n"); - } - } else if last_value == "A" { - if let Some(_last_value) = position.pop() { - json_string.push_str("]\n"); + } else if obj_type == "OA" { + while depth < position.len() - 1 { + // we need to close the positions + if let Some(last_value) = position.last() { + if last_value == "O" { + if let Some(_last_value) = position.pop() { + json_string.pop(); + json_string.pop(); + json_string.push_str("},\n"); + } + } else if last_value == "A" { + if let Some(_last_value) = position.pop() { + json_string.push_str("]\n"); + } else { + // The vector was empty, nothing to remove. + } } else { - // The vector was empty, nothing to remove. + // The last value is something else leave it } } else { - // The last value is something else leave it + // The vector is empty } - } else { - // The vector is empty } + + // we are creating a new array that will contain objects of the same type + json_string.push_str("[ \n {"); + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + // test if extra handling is needed for closing + position.insert(depth - 1, "OA".to_string()); + position.insert(depth, "O".to_string()); + } else if obj_type == "V" { + json_string.push_str(&cleanup_string(line)); + json_string.push_str(",\n"); } - // we are creating a new array that will contain objects of the same type - json_string.push_str("[ \n {"); - json_string.push_str(&cleanup_string(line)); - json_string.push(':'); - // test if extra handling is needed for closing - position.insert(depth - 1, "OA".to_string()); - position.insert(depth, "O".to_string()); - } else if obj_type == "V" { - json_string.push_str(&cleanup_string(line)); - json_string.push_str(",\n"); + i += 1; } - i += 1; + // Finalize the string to which we will concatenate new lines based on the markdown lines + json_string.pop(); + json_string = json_string.trim_end_matches(',').to_string(); + json_string.push_str("\n}"); + let parsed_json: Value = serde_json::from_str(&json_string).unwrap(); + parsed_json } - - // Finalize the string to which we will concatenate new lines based on the markdown lines - json_string.pop(); - json_string = json_string.trim_end_matches(',').to_string(); - json_string.push_str("\n}"); - let parsed_json: Value = serde_json::from_str(&json_string).unwrap(); - parsed_json } fn evaluate_line(line_to_test: &str) -> (String, usize) { From 594c19585782a0ae84ff2184a2813d0d47b8e81c Mon Sep 17 00:00:00 2001 From: hamrt Date: Tue, 5 Nov 2024 10:51:57 +0100 Subject: [PATCH 26/45] fix for ELM conversion due to lacking mapping and improvement to README.md --- README.md | 4 ++-- src/backend/routes/translate_file.rs | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c6b09fa..9fb406f 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ cargo run -- -h example: ```sh -cargo run -- -i example.json -o example_123.json -m example_mapping.json -c o-bv3-to-elm +cargo run -- -i example.json -o example_123.json -m example_mapping.json -c OBv3toELM ``` Or find the executable in the `/target/debug` folder named after the repo name `credential-converter`. @@ -47,7 +47,7 @@ Or find the executable in the `/target/debug` folder named after the repo name ` ./target/debug/credential-converter ``` ```sh -./target/debug/credential-converter -i example.json -o example_123.json -m example_mapping.json -c o-bv3-to-elm +./target/debug/credential-converter -i example.json -o example_123.json -m example_mapping.json -c OBv3toELM ``` For headless/webservice execution: diff --git a/src/backend/routes/translate_file.rs b/src/backend/routes/translate_file.rs index 2138161..8839498 100644 --- a/src/backend/routes/translate_file.rs +++ b/src/backend/routes/translate_file.rs @@ -8,7 +8,7 @@ use axum::{ }; use crate::backend::headless_cli::load_files_apply_transformations; -use crate::state::AppState; +use crate::state::{AppState, Mapping}; use std::{fs::File, io::Write, path::Path}; use tokio::fs; @@ -32,6 +32,7 @@ pub async fn translate_file(mut multipart: Multipart) -> Result Result { mapping_file_name = format!("json/mapping/custom_mapping_OBv3_ELM_latest.json"); + mapping_type = Mapping::OBv3ToELM; } "ELMToOBv3" => { mapping_file_name = format!("json/mapping/custom_mapping_ELM_OBv3_latest.json"); + mapping_type = Mapping::ELMToOBv3; } _ => { return Err(( @@ -99,6 +102,8 @@ pub async fn translate_file(mut multipart: Multipart) -> Result Date: Fri, 13 Dec 2024 18:09:37 +0100 Subject: [PATCH 27/45] add image support --- Cargo.lock | 143 +- Cargo.toml | 2 + .../custom_mapping_OBv3_ELM_latest.json | 48 +- .../custom_mapping_OBv3_ELM_latest_1.json | 224 ++ ... custom_mapping_OBv3_ELM_latest_copy.json} | 0 .../Complete_OpenBadgeCredential_image.json | 723 ++++ .../mbob_eo_eov_extracurricular_full.json | 106 + jsontest.json | 61 + ...translated_Bengales_highSchoolDiploma.json | 59 + src/backend/base64_encode.rs | 48 + src/backend/mod.rs | 1 + src/backend/repository.rs | 182 +- src/backend/transformations.rs | 20 + src/lib_2.rs | 41 + test/edubadges.png | Bin 0 -> 23704 bytes uploads/Bengales_highSchoolDiploma.json | 2925 +++++++++++++++++ 16 files changed, 4556 insertions(+), 27 deletions(-) create mode 100644 json/mapping/custom_mapping_OBv3_ELM_latest_1.json rename json/mapping/{custom_mapping_OBv3_ELM_latest copy.json => custom_mapping_OBv3_ELM_latest_copy.json} (100%) create mode 100644 json/obv3/examples/Complete_OpenBadgeCredential_image.json create mode 100755 json/obv3/examples/mbob_eo_eov_extracurricular_full.json create mode 100755 jsontest.json create mode 100755 outputs/translated_Bengales_highSchoolDiploma.json create mode 100644 src/backend/base64_encode.rs create mode 100755 src/lib_2.rs create mode 100644 test/edubadges.png create mode 100755 uploads/Bengales_highSchoolDiploma.json diff --git a/Cargo.lock b/Cargo.lock index c2a5982..682fab5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "ahash" version = "0.8.11" @@ -217,7 +223,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -228,6 +234,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit-set" version = "0.5.3" @@ -503,12 +515,22 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "credential-converter" version = "0.1.0" dependencies = [ "anyhow", "axum", + "base64 0.21.7", "clap", "color-eyre", "config", @@ -534,6 +556,7 @@ dependencies = [ "tracing", "tracing-error", "tracing-subscriber", + "ureq", ] [[package]] @@ -733,6 +756,16 @@ dependencies = [ "regex", ] +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + [[package]] name = "fluent-uri" version = "0.2.0-alpha.5" @@ -1218,7 +1251,7 @@ checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978" dependencies = [ "ahash", "anyhow", - "base64", + "base64 0.21.7", "bytecount", "clap", "fancy-regex", @@ -1346,6 +1379,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "0.8.11" @@ -1804,7 +1846,7 @@ version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", @@ -1834,13 +1876,28 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "ron" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ - "base64", + "base64 0.21.7", "bitflags 2.6.0", "serde", "serde_derive", @@ -1913,6 +1970,38 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustls" +version = "0.23.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -2168,6 +2257,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.68" @@ -2630,6 +2725,31 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "encoding_rs", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots", +] + [[package]] name = "url" version = "2.5.2" @@ -2766,6 +2886,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.26.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3001,3 +3130,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml index a93849e..9518151 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ edition = "2021" clap = { version = "4", features = ["derive"] } jsonschema = "0.17" tokio = { version = "1", features = ["full"] } +ureq = { version = "*", features = ["json", "charset"] } +base64 = "0.21" serde = { version = "1", features = ["serde_derive", "derive"] } serde_json = "1" anyhow = "1.0" diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index 1b4d4f4..589b00e 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -22,10 +22,9 @@ } }, { - "type_": "copy", + "type_": "stringArrayIt", "source": { - "format": "OBv3", - "path": "$.type" + "value": ["VerifiableCredential", "VerifiableAttestation", "EuropeanDigitalCredential"] }, "destination": { "format": "ELM", @@ -69,22 +68,43 @@ "type_": "copy", "source": { "format": "OBv3", - "path": "$.credentialSubject.id" + "path": "$.validFrom" }, "destination": { "format": "ELM", - "path": "$.credentialSubject.id" + "path": "$.issuanceDate" } }, { "type_": "copy", "source": { "format": "OBv3", - "path": "$.proof" + "path": "$.validFrom" }, "destination": { "format": "ELM", - "path": "$.proof" + "path": "$.issued" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.id" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.id" + } + }, + { + "type_": "stringit", + "source": { + "value": "Person" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.type" } }, { @@ -98,6 +118,17 @@ "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome[0]" } }, + { + "type_": "imageToIndividualDisplay", + "source": { + "format": "OBv3", + "path": "$.image" + }, + "destination": { + "format": "ELM", + "path": "$.displayParameter" + } + }, { "type_": "copy", "source": { @@ -174,7 +205,7 @@ }, "destination": { "format": "ELM", - "path": "$.credentialSubject.givenName" + "path": "$.credentialSubject.givenName.en[0]" } }, { @@ -201,5 +232,4 @@ "path": "$.credentialSubject.fullName" } } - ] \ No newline at end of file diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest_1.json b/json/mapping/custom_mapping_OBv3_ELM_latest_1.json new file mode 100644 index 0000000..55eaa6b --- /dev/null +++ b/json/mapping/custom_mapping_OBv3_ELM_latest_1.json @@ -0,0 +1,224 @@ +[ + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.@context" + }, + "destination": { + "format": "ELM", + "path": "$.@context" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.id" + }, + "destination": { + "format": "ELM", + "path": "$.id" + } + }, + { + "type_": "stringArrayIt", + "source": { + "value": ["VerifiableCredential", "VerifiableAttestation", "EuropeanDigitalCredential"] + }, + "destination": { + "format": "ELM", + "path": "$.type" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.issuer.id" + }, + "destination": { + "format": "ELM", + "path": "$.issuer.id" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.issuer.name" + }, + "destination": { + "format": "ELM", + "path": "$.issuer.legalName.en" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.validFrom" + }, + "destination": { + "format": "ELM", + "path": "$.validFrom" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.validFrom" + }, + "destination": { + "format": "ELM", + "path": "$.issuanceDate" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.validFrom" + }, + "destination": { + "format": "ELM", + "path": "$.issued" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.id" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.id" + } + }, + { + "type_": "stringit", + "source": { + "value": "Person" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.type" + } + }, + { + "type_": "markdownToJson", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.criteria.narrative" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome[0]" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.name" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].title.en[0]" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.id" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].id" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.name" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.title.en[0]" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSchema" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSchema" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialStatus" + }, + "destination": { + "format": "ELM", + "path": "$.credentialStatus" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "Student ID", + "path": "$.credentialSubject.identifier[0]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.identifier" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "ext:givenName", + "path": "$.credentialSubject.identifier[1]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.givenName.en[0]" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "ext:familyName", + "path": "$.credentialSubject.identifier[2]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.givenName" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "ext:fullName", + "path": "$.credentialSubject.identifier[3]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.fullName" + } + } +] \ No newline at end of file diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest copy.json b/json/mapping/custom_mapping_OBv3_ELM_latest_copy.json similarity index 100% rename from json/mapping/custom_mapping_OBv3_ELM_latest copy.json rename to json/mapping/custom_mapping_OBv3_ELM_latest_copy.json diff --git a/json/obv3/examples/Complete_OpenBadgeCredential_image.json b/json/obv3/examples/Complete_OpenBadgeCredential_image.json new file mode 100644 index 0000000..87dcf8c --- /dev/null +++ b/json/obv3/examples/Complete_OpenBadgeCredential_image.json @@ -0,0 +1,723 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://purl.imsglobal.org/spec/ob/v3p0/extensions.json" + ], + "id": "http://1edtech.edu/credentials/3732", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "1EdTech University Degree for Example Student", + "description": "1EdTech University Degree Description", + "image": { + "id": "https://avatars.githubusercontent.com/u/22613412?v=4", + "type": "Image", + "caption": "1EdTech University Degree for Example Student" + }, + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "activityEndDate": "2010-01-02T00:00:00Z", + "activityStartDate": "2010-01-01T00:00:00Z", + "creditsEarned": 42, + "licenseNumber": "A-9320041", + "role": "Major Domo", + "source": { + "id": "https://school.edu/issuers/201234", + "type": [ + "Profile" + ], + "name": "1EdTech College of Arts" + }, + "term": "Fall", + "identifier": [ + { + "type": "IdentityObject", + "identityHash": "student@1edtech.edu", + "identityType": "emailAddress", + "hashed": false, + "salt": "not-used" + }, + { + "type": "IdentityObject", + "identityHash": "somebody@gmail.com", + "identityType": "emailAddress", + "hashed": false, + "salt": "not-used" + } + ], + "achievement": { + "id": "https://1edtech.edu/achievements/degree", + "type": [ + "Achievement" + ], + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "degree", + "targetDescription": "1EdTech University Degree programs.", + "targetName": "1EdTech University Degree", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFItem", + "targetUrl": "https://1edtech.edu/catalog/degree" + }, + { + "type": [ + "Alignment" + ], + "targetCode": "degree", + "targetDescription": "1EdTech University Degree programs.", + "targetName": "1EdTech University Degree", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CTDL", + "targetUrl": "https://credentialengineregistry.org/resources/ce-98cb027b-95ef-4494-908d-6f7790ec6b6b" + } + ], + "achievementType": "Degree", + "creator": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "1EdTech University", + "url": "https://1edtech.edu", + "phone": "1-222-333-4444", + "description": "1EdTech University provides online degree programs.", + "endorsement": [ + { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "id": "http://1edtech.edu/endorsementcredential/3732", + "type": [ + "VerifiableCredential", + "EndorsementCredential" + ], + "name": "SDE endorsement", + "issuer": { + "id": "https://accrediter.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example Accrediting Agency" + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2020-01-01T00:00:00Z", + "credentialSubject": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "EndorsementSubject" + ], + "endorsementComment": "1EdTech University is in good standing" + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://accrediter.edu/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialStatus": { + "id": "https://1edtech.edu/credentials/3732/revocations", + "type": "1EdTechRevocationList" + }, + "refreshService": { + "id": "http://1edtech.edu/credentials/3732", + "type": "1EdTechCredentialRefresh" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-rdf-2022", + "created": "2022-05-26T18:17:08Z", + "verificationMethod": "https://accrediter.edu/issuers/565049#zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA", + "proofPurpose": "assertionMethod", + "proofValue": "zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA" + } + ] + }, + { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "id": "http://1edtech.edu/endorsementcredential/3733", + "type": [ + "VerifiableCredential", + "EndorsementCredential" + ], + "name": "SDE endorsement", + "issuer": { + "id": "https://state.gov/issuers/565049", + "type": [ + "Profile" + ], + "name": "State Department of Education" + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2020-01-01T00:00:00Z", + "credentialSubject": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "EndorsementSubject" + ], + "endorsementComment": "1EdTech University is in good standing" + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://state.gov/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialStatus": { + "id": "https://state.gov/credentials/3732/revocations", + "type": "1EdTechRevocationList" + }, + "refreshService": { + "id": "http://state.gov/credentials/3732", + "type": "1EdTechCredentialRefresh" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-rdf-2022", + "created": "2022-05-26T18:25:59Z", + "verificationMethod": "https://accrediter.edu/issuers/565049#z5bDnmSgDczXwZGya6ZjxKaxkdKxzsCMiVSsgEVWxnaWK7ZqbKnzcCd7mUKE9DQaAL2QMXP5AquPeW6W2CWrZ7jNC", + "proofPurpose": "assertionMethod", + "proofValue": "z5bDnmSgDczXwZGya6ZjxKaxkdKxzsCMiVSsgEVWxnaWK7ZqbKnzcCd7mUKE9DQaAL2QMXP5AquPeW6W2CWrZ7jNC" + } + ] + } + ], + "image": { + "id": "https://1edtech.edu/logo.png", + "type": "Image", + "caption": "1EdTech University logo" + }, + "email": "registrar@1edtech.edu", + "address": { + "type": [ + "Address" + ], + "addressCountry": "USA", + "addressCountryCode": "US", + "addressRegion": "TX", + "addressLocality": "Austin", + "streetAddress": "123 First St", + "postOfficeBoxNumber": "1", + "postalCode": "12345", + "geo": { + "type": "GeoCoordinates", + "latitude": 1, + "longitude": 1 + } + }, + "otherIdentifier": [ + { + "type": "IdentifierEntry", + "identifier": "12345", + "identifierType": "sourcedId" + }, + { + "type": "IdentifierEntry", + "identifier": "67890", + "identifierType": "nationalIdentityNumber" + } + ], + "official": "Horace Mann", + "parentOrg": { + "id": "did:example:123456789", + "type": [ + "Profile" + ], + "name": "Universal Universities" + } + }, + "creditsAvailable": 36, + "criteria": { + "id": "https://1edtech.edu/achievements/degree", + "narrative": "# Degree Requirements\nStudents must complete..." + }, + "description": "1EdTech University Degree Description", + "endorsement": [ + { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "id": "http://1edtech.edu/endorsementcredential/3734", + "type": [ + "VerifiableCredential", + "EndorsementCredential" + ], + "name": "EAA endorsement", + "issuer": { + "id": "https://accrediter.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example Accrediting Agency" + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2020-01-01T00:00:00Z", + "credentialSubject": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "EndorsementSubject" + ], + "endorsementComment": "1EdTech University is in good standing" + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://accrediter.edu/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialStatus": { + "id": "https://1edtech.edu/credentials/3732/revocations", + "type": "1EdTechRevocationList" + }, + "refreshService": { + "id": "http://1edtech.edu/credentials/3732", + "type": "1EdTechCredentialRefresh" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-rdf-2022", + "created": "2022-05-26T18:17:08Z", + "verificationMethod": "https://accrediter.edu/issuers/565049#zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA", + "proofPurpose": "assertionMethod", + "proofValue": "zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA" + } + ] + } + ], + "fieldOfStudy": "Research", + "humanCode": "R1", + "image": { + "id": "https://1edtech.edu/achievements/degree/image", + "type": "Image", + "caption": "1EdTech University Degree" + }, + "name": "1EdTech University Degree", + "otherIdentifier": [ + { + "type": "IdentifierEntry", + "identifier": "abde", + "identifierType": "identifier" + } + ], + "resultDescription": [ + { + "id": "urn:uuid:f6ab24cd-86e8-4eaf-b8c6-ded74e8fd41c", + "type": [ + "ResultDescription" + ], + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "project", + "targetDescription": "Project description", + "targetName": "Final Project", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFItem", + "targetUrl": "https://1edtech.edu/catalog/degree/project" + } + ], + "allowedValue": [ + "D", + "C", + "B", + "A" + ], + "name": "Final Project Grade", + "requiredValue": "C", + "resultType": "LetterGrade" + }, + { + "id": "urn:uuid:a70ddc6a-4c4a-4bd8-8277-cb97c79f40c5", + "type": [ + "ResultDescription" + ], + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "project", + "targetDescription": "Project description", + "targetName": "Final Project", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFItem", + "targetUrl": "https://1edtech.edu/catalog/degree/project" + } + ], + "allowedValue": [ + "D", + "C", + "B", + "A" + ], + "name": "Final Project Grade", + "requiredLevel": "urn:uuid:d05a0867-d0ad-4b03-bdb5-28fb5d2aab7a", + "resultType": "RubricCriterionLevel", + "rubricCriterionLevel": [ + { + "id": "urn:uuid:d05a0867-d0ad-4b03-bdb5-28fb5d2aab7a", + "type": [ + "RubricCriterionLevel" + ], + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "project", + "targetDescription": "Project description", + "targetName": "Final Project", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFRubricCriterionLevel", + "targetUrl": "https://1edtech.edu/catalog/degree/project/rubric/levels/mastered" + } + ], + "description": "The author demonstrated...", + "level": "Mastered", + "name": "Mastery", + "points": "4" + }, + { + "id": "urn:uuid:6b84b429-31ee-4dac-9d20-e5c55881f80e", + "type": [ + "RubricCriterionLevel" + ], + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "project", + "targetDescription": "Project description", + "targetName": "Final Project", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFRubricCriterionLevel", + "targetUrl": "https://1edtech.edu/catalog/degree/project/rubric/levels/basic" + } + ], + "description": "The author demonstrated...", + "level": "Basic", + "name": "Basic", + "points": "4" + } + ] + }, + { + "id": "urn:uuid:b07c0387-f2d6-4b65-a3f4-f4e4302ea8f7", + "type": [ + "ResultDescription" + ], + "name": "Project Status", + "resultType": "Status" + } + ], + "specialization": "Computer Science Research", + "tag": [ + "research", + "computer science" + ] + }, + "image": { + "id": "https://1edtech.edu/credentials/3732/image", + "type": "Image", + "caption": "1EdTech University Degree for Example Student" + }, + "narrative": "There is a final project report and source code evidence.", + "result": [ + { + "type": [ + "Result" + ], + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "project", + "targetDescription": "Project description", + "targetName": "Final Project", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFItem", + "targetUrl": "https://1edtech.edu/catalog/degree/project/result/1" + } + ], + "resultDescription": "urn:uuid:f6ab24cd-86e8-4eaf-b8c6-ded74e8fd41c", + "value": "A" + }, + { + "type": [ + "Result" + ], + "achievedLevel": "urn:uuid:d05a0867-d0ad-4b03-bdb5-28fb5d2aab7a", + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "project", + "targetDescription": "Project description", + "targetName": "Final Project", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFItem", + "targetUrl": "https://1edtech.edu/catalog/degree/project/result/1" + } + ], + "resultDescription": "urn:uuid:f6ab24cd-86e8-4eaf-b8c6-ded74e8fd41c" + }, + { + "type": [ + "Result" + ], + "resultDescription": "urn:uuid:f6ab24cd-86e8-4eaf-b8c6-ded74e8fd41c", + "status": "Completed" + } + ] + }, + "endorsement": [ + { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "id": "http://1edtech.edu/endorsementcredential/3735", + "type": [ + "VerifiableCredential", + "EndorsementCredential" + ], + "name": "EAA endorsement", + "issuer": { + "id": "https://accrediter.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example Accrediting Agency" + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2020-01-01T00:00:00Z", + "credentialSubject": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "EndorsementSubject" + ], + "endorsementComment": "1EdTech University is in good standing" + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://accrediter.edu/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialStatus": { + "id": "https://1edtech.edu/credentials/3732/revocations", + "type": "1EdTechRevocationList" + }, + "refreshService": { + "id": "http://1edtech.edu/credentials/3732", + "type": "1EdTechCredentialRefresh" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-rdf-2022", + "created": "2022-05-26T18:17:08Z", + "verificationMethod": "https://accrediter.edu/issuers/565049#zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA", + "proofPurpose": "assertionMethod", + "proofValue": "zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA" + } + ] + } + ], + "evidence": [ + { + "id": "https://1edtech.edu/credentials/3732/evidence/1", + "type": [ + "Evidence" + ], + "narrative": "# Final Project Report \n This project was ...", + "name": "Final Project Report", + "description": "This is the final project report.", + "genre": "Research", + "audience": "Department" + }, + { + "id": "https://github.com/somebody/project", + "type": [ + "Evidence" + ], + "name": "Final Project Code", + "description": "This is the source code for the final project app.", + "genre": "Research", + "audience": "Department" + } + ], + "issuer": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "1EdTech University", + "url": "https://1edtech.edu", + "phone": "1-222-333-4444", + "description": "1EdTech University provides online degree programs.", + "endorsement": [ + { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "id": "http://1edtech.edu/endorsementcredential/3736", + "type": [ + "VerifiableCredential", + "EndorsementCredential" + ], + "name": "EAA endorsement", + "issuer": { + "id": "https://accrediter.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example Accrediting Agency" + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2020-01-01T00:00:00Z", + "credentialSubject": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "EndorsementSubject" + ], + "endorsementComment": "1EdTech University is in good standing" + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://accrediter.edu/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialStatus": { + "id": "https://1edtech.edu/credentials/3732/revocations", + "type": "1EdTechRevocationList" + }, + "refreshService": { + "id": "http://1edtech.edu/credentials/3732", + "type": "1EdTechCredentialRefresh" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-rdf-2022", + "created": "2022-05-26T18:17:08Z", + "verificationMethod": "https://accrediter.edu/issuers/565049#zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA", + "proofPurpose": "assertionMethod", + "proofValue": "zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA" + } + ] + } + ], + "image": { + "id": "https://1edtech.edu/logo.png", + "type": "Image", + "caption": "1EdTech University logo" + }, + "email": "registrar@1edtech.edu", + "address": { + "type": [ + "Address" + ], + "addressCountry": "USA", + "addressCountryCode": "US", + "addressRegion": "TX", + "addressLocality": "Austin", + "streetAddress": "123 First St", + "postOfficeBoxNumber": "1", + "postalCode": "12345", + "geo": { + "type": "GeoCoordinates", + "latitude": 1, + "longitude": 1 + } + }, + "otherIdentifier": [ + { + "type": "IdentifierEntry", + "identifier": "12345", + "identifierType": "sourcedId" + }, + { + "type": "IdentifierEntry", + "identifier": "67890", + "identifierType": "nationalIdentityNumber" + } + ], + "official": "Horace Mann", + "parentOrg": { + "id": "did:example:123456789", + "type": [ + "Profile" + ], + "name": "Universal Universities" + } + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2030-01-01T00:00:00Z", + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialStatus": { + "id": "https://1edtech.edu/credentials/3732/revocations", + "type": "1EdTechRevocationList" + }, + "refreshService": { + "id": "http://1edtech.edu/credentials/3732", + "type": "1EdTechCredentialRefresh" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "created": "2024-05-31T14:05:25Z", + "verificationMethod": "https://1edtech.edu/issuers/565049#z6MkphU6QmojC6GdUBNYypgnGaiL2TLisLMxpE1oZcmKg7Ad", + "cryptosuite": "eddsa-rdfc-2022", + "proofPurpose": "assertionMethod", + "proofValue": "z5A4ZXLJa4dUArTmpdP9vnrYijMLCT1tR9KWaFmLT2PeQp3gSnGA9wrRJqrJ5Z8YnpVDxZQWRGjjWNbj2PKDJe7dt" + } + ] +} diff --git a/json/obv3/examples/mbob_eo_eov_extracurricular_full.json b/json/obv3/examples/mbob_eo_eov_extracurricular_full.json new file mode 100755 index 0000000..09ce3d8 --- /dev/null +++ b/json/obv3/examples/mbob_eo_eov_extracurricular_full.json @@ -0,0 +1,106 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://raw.githubusercontent.com/educredentials/obv3-examples/refs/heads/main/contexts/educredential.json" + ], + "id": "http://example.com/credentials/crd-A1B2C3", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "https://example.com/issuers/iss-4B0B1A", + "type": [ + "Profile" + ], + "name": "MBO Beek", + "otherIdentifier": [ + { + "type": "IdentifierEntry", + "identifier": "4B0B1A", + "identifierType": "ext:BRIN" + }, + { + "type": "IdentifierEntry", + "identifier": "mbobeek.example.edu", + "identifierType": "name" + } + ] + }, + "validFrom": "2024-08-30T00:00:00Z", + "validUntil": "2029-08-30T00:00:00Z", + "name": "Extracurriculair Stage bij het luchtverkopersgilde", + "credentialSubject": { + "id": "https://example.com/credentials/stu-A1B2C3", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/ach-33D4E5", + "type": [ + "Achievement", + "EducredentialAchievement" + ], + "criteria": { + "narrative": "De student leert de kunst van het verkopen van niet-bestaande producten en diensten." + }, + "description": "Leer hoe je iets verkoopt dat eigenlijk niet bestaat, met praktijkoefeningen in het verkopen van luchtkastelen, abonnementen op niets.", + "name": "Extracurriculair Stage bij het luchtverkopersgilde", + "image": { + "id": "https://static.example.com/luchtkastelen.jpg", + "type": "Image" + }, + "inLanguage": "nl-NL", + "educationProgramIdentifier": 20121344, + "SBU": 120, + "participationType": "onsite or blended", + "assessmentType": "application of a skill", + "supervisionType": "onsite with identity verification", + "identityChecked": false, + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetType": "ext:EQF", + "targetName": "EQF level 3", + "targetCode": "3", + "targetUrl": "https://content.example.com/description-eqf-levels" + } + ], + "resultDescription": [ + { + "id": "https://example.com/results/ects-nl-NL-A1B2C3", + "type": [ + "ResultDescription" + ], + "valueMax": "10", + "valueMin": "1", + "name": "Final Project Grade", + "requiredValue": "6", + "resultType": "ext:ECTSGradeScore" + } + ] + }, + "result": [ + { + "type": [ + "Result" + ], + "resultDescription": "https://example.com/results/ects-nl-NL-A1B2C3", + "value": "7.5" + } + ] + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://raw.githubusercontent.com/educredentials/obv3-examples/refs/heads/main/schemas/extracurricular.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ] +} diff --git a/jsontest.json b/jsontest.json new file mode 100755 index 0000000..619c061 --- /dev/null +++ b/jsontest.json @@ -0,0 +1,61 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "http://data.europa.eu/snb/model/context/edc-ap" + ], + "credentialSchema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialSubject": { + "hasClaim": [ + { + "id": "https://example.com/achievements/21st-century-skills/teamwork", + "specifiedBy": { + "learningOutcome": [ + "Team members are nominated for this badge by their peers and recognized upon review by Example Corp management." + ], + "title": { + "en": [ + "Teamwork" + ] + } + }, + "title": { + "en": [ + "Teamwork Badge" + ] + } + } + ], + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21" + }, + "id": "http://example.com/credentials/3527", + "issuer": { + "id": "https://example.com/issuers/876543", + "legalName": { + "en": "Example Corp" + } + }, + "proof": [ + { + "created": "2024-05-31T14:05:25Z", + "cryptosuite": "eddsa-rdfc-2022", + "proofPurpose": "assertionMethod", + "proofValue": "zJPcuWH556eQMEiPVd1Emp85PNTgsRyVYbMcxjsQXJ4MBQ2yEn83CQyJDjpvUSNx8GTSpiCC5pdYBou5gyTvnSwx", + "type": "DataIntegrityProof", + "verificationMethod": "https://example.com/issuers/876543#z6MksaRBVxaAjk4dNYcw5tReeHK6VAVVpjTyAzb4NofmhoGi" + } + ], + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "validFrom": "2010-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/outputs/translated_Bengales_highSchoolDiploma.json b/outputs/translated_Bengales_highSchoolDiploma.json new file mode 100755 index 0000000..b20d4a2 --- /dev/null +++ b/outputs/translated_Bengales_highSchoolDiploma.json @@ -0,0 +1,59 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://accrediter.edu/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialSubject": { + "achievement": { + "id": "urn:epass:learningAchievement:6", + "name": "German International Abitur", + "type": [ + "Achievement" + ] + }, + "familyName": "Smith", + "fullName": "David Smith", + "givenName": "David", + "id": "did:key:afsdlkj34134", + "identifier": [ + { + "hashed": false, + "identityHash": "5547554", + "identityType": "ext:studentID", + "salt": "not-used", + "type": "IdentityObject" + } + ], + "type": [ + "AchievementSubject", + "profile" + ] + }, + "id": "urn:credential:87ddce4d-de74-4838-b7a4-1f14d9c614ec", + "issuer": { + "address": { + "addressCountryCode": "http://publications.europa.eu/resource/authority/country/ESP" + }, + "id": "did:ebsi:org:12345689", + "name": "ORGANIZACION TEST", + "type": [ + "Profile" + ] + }, + "name": "Higher Education Entrance Qualification - German International ABITUR School", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "validFrom": "2020-07-20T00:00:00+02:00" +} \ No newline at end of file diff --git a/src/backend/base64_encode.rs b/src/backend/base64_encode.rs new file mode 100644 index 0000000..9eaef61 --- /dev/null +++ b/src/backend/base64_encode.rs @@ -0,0 +1,48 @@ +use num_traits::ops::bytes; +//use reqwest::Client; +//use reqwest::blocking::get; +use base64::{engine::general_purpose::STANDARD as Base64Engine, Engine}; +use std::error::Error; +use std::io::{BufReader, Read, copy}; +use std::fs::File; +use ureq::get; + +/// Fetches an image from the given URL, encodes it in Base64, and returns the encoded string. +/// +/// # Arguments +/// - `url`: The URL of the image to encode. +/// +/// # Returns +/// - `Ok(String)`: The Base64-encoded string of the image if successful. +/// - `Err(Box)`: An error if the fetch or encoding fails. +pub fn encode_image_from_url(url: &str) -> Result> { + + let filename = "downloaded_image.jpg"; + let resp = get(url).call().expect("Failed to download image"); + + assert!(resp.has("Content-Length")); + let len: usize = resp.header("Content-Length") + .unwrap() + .parse()?; + + let mut bytes: Vec = Vec::with_capacity(len); + resp.into_reader() + .take(10_000_000) + .read_to_end(&mut bytes)?; + + // let mut file = File::create(filename).expect("Failed to create file"); + // copy(&mut resp.into_reader(), &mut file).expect("Failed to save image"); + + // let mut reader = BufReader::new(file); + // let mut image_bytes : Vec = Vec::new(); + // reader.read_to_end(&mut image_bytes).unwrap(); + // println!("{:#?}", image_bytes); + + + // Encode the image bytes as a Base64 string + let base64_string = Base64Engine.encode(&bytes); + + Ok(base64_string) +} + + diff --git a/src/backend/mod.rs b/src/backend/mod.rs index e09ddcc..97bf321 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -11,3 +11,4 @@ pub mod routes; pub mod transformations; pub mod update_display; pub mod web; +pub mod base64_encode; diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 50148c8..81fbb85 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -3,6 +3,7 @@ use crate::{ jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, transformations::{DataLocation, DataTypeLocation, StringArrayValue, StringValue, Transformation}, + base64_encode::encode_image_from_url, }, state::{AppState, Mapping}, trace_dbg, @@ -377,6 +378,55 @@ impl Repository { Some((destination_path, source_path)) } + Transformation::ImageToIndividualDisplay { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + + // run the source value through a markdown converter to fit the nested objects into a markdown string + let markdown_source_value = json!(image_to_individual_display(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(markdown_source_value); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + + _ => todo!(), } } @@ -521,6 +571,107 @@ fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { } } + +fn image_to_individual_display(image_value: Value) -> Value { + //inspect the image object and re write it so it can be reused in ELM + + //we need to achieve the following structure into the indivudualDisplay array: + let json_data = r#" + { + "id": "urn:epass:individualDisplay:c05743e7-9f9d-4e0b-899b-7ae6514c7a02", + "type": "IndividualDisplay", + "language": { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + }, + "displayDetail": [ + { + "id": "urn:epass:displayDetail:123", + "type": "DisplayDetail", + "image": { + "id": "urn:epass:mediaObject:https://avatars.githubusercontent.com/u/22613412?v=4", + "type": "MediaObject", + "content": "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", + "contentEncoding": { + "id": "http://data.europa.eu/snb/encoding/6146cde7dd", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/encoding/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["base64"] + } + }, + "contentType": { + "id": "http://publications.europa.eu/resource/authority/file-type/PNG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/file-type", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["PNG"] + }, + "notation": "file-type" + } + } + } + ] + } + "#; + + + let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); + + // Directly mutate the `content` value + // first try to encode the image in the URL: + let encoded_string = match encode_image_from_url("https://avatars.githubusercontent.com/u/22613412?v=4") { + Ok(encoded_string) => { + println!("Successfully encoded the image."); + encoded_string // Assign the encoded string to the variable + } + Err(e) => { + eprintln!("Error: {}", e); + String::new() // Assign an empty string or a default value in case of an error + } + }; + if let Some(image_content) = parsed_json["displayDetail"][0]["image"]["content"].as_str() { + parsed_json["displayDetail"][0]["image"]["content"] = Value::String(encoded_string); + } else { + println!("Key 'id' in 'language' not found."); + } + + + + println!("{:#?}", parsed_json); + parsed_json + + // if let Some(id_value) = identity_value.get("identityHash") { + // if identity_type.eq(&"Student ID".to_string()) { + // let mut new_object = Map::new(); + // new_object.insert("id".to_string(), Value::String("urn:epass:identifier:2".to_string())); + // new_object.insert("type".to_string(), Value::String("Identifier".to_string())); + // new_object.insert("notation".to_string(), id_value.clone()); + // new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); + // let _current_value = Value::Object(new_object); + // _current_value + // } else { + // id_value.clone() + // } + // } else { + // Value::String("".to_string()) + // } +} + fn json_to_markdown(json: &Value, indent_level: usize) -> String { let mut markdown = String::new(); let indent = " ".repeat(indent_level); @@ -555,8 +706,24 @@ fn json_to_markdown(json: &Value, indent_level: usize) -> String { markdown } -/// Recursively converts indented lines of Markdown into a JSON structure. fn markdown_to_json(lines: &[&str]) -> Value { + +// Recursively converts indented lines of Markdown into a JSON structure. +// 1. Parsing Markdown: +// • Headings (#): These are treated as keys in the resulting JSON object. +// • Bold Text (**): This is also treated as a key in the JSON object. +// • List Items (-): These are treated as elements in a JSON array. +// • Plain Text: If it’s not part of a list or a key, it’s treated as a value associated with the last key in the current JSON object. +// 2. Indentation Handling: +// • The code tracks the current indentation level of the Markdown. If the indentation increases, it means a new nested structure (object or array) is starting. If it decreases, the last completed structure is attached to the parent object or array. +// 3. Stack Management: +// • A stack is used to manage the nested structure. Each time a new nested object or array is detected, it’s pushed onto the stack. Once the nesting ends (indentation decreases), the structure is popped from the stack and integrated into the parent structure. +// 4. Regex Patterns: +// • heading_regex: Matches Markdown headings (e.g., # Title). +// • bold_regex: Matches bolded keys (e.g., **Key**:). +// • list_item_regex: Matches list items (e.g., - item). + + let mut i = 0; let mut position: Vec = Vec::new(); @@ -778,16 +945,3 @@ fn cleanup_string(string_to_clean: &str) -> String { format!("\"{}\"", cleaned_string) } -// 1. Parsing Markdown: -// • Headings (#): These are treated as keys in the resulting JSON object. -// • Bold Text (**): This is also treated as a key in the JSON object. -// • List Items (-): These are treated as elements in a JSON array. -// • Plain Text: If it’s not part of a list or a key, it’s treated as a value associated with the last key in the current JSON object. -// 2. Indentation Handling: -// • The code tracks the current indentation level of the Markdown. If the indentation increases, it means a new nested structure (object or array) is starting. If it decreases, the last completed structure is attached to the parent object or array. -// 3. Stack Management: -// • A stack is used to manage the nested structure. Each time a new nested object or array is detected, it’s pushed onto the stack. Once the nesting ends (indentation decreases), the structure is popped from the stack and integrated into the parent structure. -// 4. Regex Patterns: -// • heading_regex: Matches Markdown headings (e.g., # Title). -// • bold_regex: Matches bolded keys (e.g., **Key**:). -// • list_item_regex: Matches list items (e.g., - item). diff --git a/src/backend/transformations.rs b/src/backend/transformations.rs index 57f854e..fb1964c 100644 --- a/src/backend/transformations.rs +++ b/src/backend/transformations.rs @@ -179,6 +179,21 @@ impl IdentifierToObject { } } + +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum ImageToIndividualDisplay { + imageToIndividualDisplay, +} + +impl ImageToIndividualDisplay { + pub fn apply(&self, value: Value) -> Value { + match self { + ImageToIndividualDisplay::imageToIndividualDisplay => value, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum Transformation { @@ -217,6 +232,11 @@ pub enum Transformation { source: DataTypeLocation, destination: DataLocation, }, + ImageToIndividualDisplay { + type_: ImageToIndividualDisplay, + source: DataLocation, + destination: DataLocation, + }, OneToMany { type_: OneToMany, source: DataLocation, diff --git a/src/lib_2.rs b/src/lib_2.rs new file mode 100755 index 0000000..04a09d6 --- /dev/null +++ b/src/lib_2.rs @@ -0,0 +1,41 @@ +use std::net::SocketAddr; + +use eyre::Result; +use routes::create_router; + +mod routes; + +pub struct Server { + address: [u8; 4], + port: u16, +} + +impl Server { + pub fn new() -> Self { + let address = [127, 0, 0, 1]; + let port = 3000; + + Self { address, port } + } + + pub async fn run(&self) -> Result<()> { + tracing_subscriber::fmt::init(); + + let app = create_router(); + let address = SocketAddr::from((self.address, self.port)); + + tracing::info!("server running on port: {}", self.port); + + // run our app with hyper, listening globally on port 3000 + let listener = tokio::net::TcpListener::bind(address).await.unwrap(); + axum::serve(listener, app).await.unwrap(); + + Ok(()) + } +} + +impl Default for Server { + fn default() -> Self { + Self::new() + } +} diff --git a/test/edubadges.png b/test/edubadges.png new file mode 100644 index 0000000000000000000000000000000000000000..b963ca33c6f98a1188c582473a82ccc4340fcf16 GIT binary patch literal 23704 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

?`rjMJLGs^?0h)Orr2;SQV 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 literal 0 HcmV?d00001 diff --git a/uploads/Bengales_highSchoolDiploma.json b/uploads/Bengales_highSchoolDiploma.json new file mode 100755 index 0000000..c9e80e7 --- /dev/null +++ b/uploads/Bengales_highSchoolDiploma.json @@ -0,0 +1,2925 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "http://data.europa.eu/snb/model/context/edc-ap" + ], + "id": "urn:credential:87ddce4d-de74-4838-b7a4-1f14d9c614ec", + "type": [ + "VerifiableCredential", + "VerifiableAttestation", + "EuropeanDigitalCredential" + ], + "credentialSchema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialSubject": { + "id": "did:key:afsdlkj34134", + "type": "Person", + "identifier": [ + { + "id": "urn:epass:identifier:2", + "type": "Identifier", + "notation": "5547554", + "schemeName": "Student ID" + } + ], + "givenName": { + "en": ["David"] + }, + "familyName": { + "en": ["Smith"] + }, + "fullName": { + "en": ["David Smith"] + }, + "hasClaim": [ + { + "id": "urn:epass:learningAchievement:6", + "type": "LearningAchievement", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": [ + "Higher Education Entrance Qualification - German International ABITUR School" + ] + }, + "entitlesTo": [ + { + "id": "urn:epass:entitlement:1", + "type": "LearningEntitlement", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "description": { + "en": [ + "The owner completed the Higher Education Entrace Qualification for admission to Higher Education instititutions" + ] + }, + "title": { + "en": ["Passed the German International Abitur"] + }, + "specifiedBy": { + "id": "urn:epass:learningEntitlementSpec:1", + "type": "LearningEntitlementSpecification", + "title": { + "en": ["Passed the German International Abitur"] + }, + "dcType": [ + { + "id": "http://data.europa.eu/snb/entitlement/64aad92881", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/entitlement/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["learning opportunity"] + } + } + ], + "entitlementStatus": { + "id": "http://data.europa.eu/snb/entitlement-status/b7015a8a8c", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/entitlement-status/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["actual"] + } + } + } + } + ], + "hasPart": [ + { + "id": "urn:epass:learningAchievement:1", + "type": "LearningAchievement", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "description": { + "en": [ + "Individual results achieved during the Qualilification phase Q" + ] + }, + "title": { + "en": ["History"] + }, + "provenBy": [ + { + "id": "urn:epass:learningAssessment:1", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["History semester 3"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:1", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["History semester 3"] + } + } + }, + { + "id": "urn:epass:learningAssessment:2", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["History semester 1"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:2", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["History semester 1"] + } + } + }, + { + "id": "urn:epass:learningAssessment:3", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["History semester 2"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:3", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["History semester 2"] + } + } + }, + { + "id": "urn:epass:learningAssessment:4", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["History semester 4"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:4", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["History semester 4"] + } + } + } + ], + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "type": "LearningAchievementSpecification", + "additionalNote": [ + { + "id": "urn:epass:note:3", + "type": "Note", + "noteLiteral": { + "en": [ + "ISCED 0232: Literature and linguistics. Currently no specific code for native language(?) (Aktuell kein spezifischer code für Muttersprache(?))" + ] + }, + "subject": { + "id": "urn:epass:concept:dbd7241e-e450-4160-84a3-cc4be281a773", + "type": "Concept", + "inScheme": { + "id": "urn:epass:conceptScheme:66cfdba4-db7f-4257-bd24-3ea52844d50d", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["More Information"] + } + } + } + ], + "title": { + "en": ["History"] + } + } + }, + { + "id": "urn:epass:learningAchievement:2", + "type": "LearningAchievement", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "description": { + "en": [ + "Individual results achieved during the Qualilification phase Q" + ] + }, + "title": { + "en": ["Art"] + }, + "provenBy": [ + { + "id": "urn:epass:learningAssessment:5", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["Art semester 1"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:5", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["Art semester 1"] + } + } + }, + { + "id": "urn:epass:learningAssessment:6", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["Art semester 2"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:6", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["Art semester 2"] + } + } + }, + { + "id": "urn:epass:learningAssessment:7", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["Art semester 3"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:7", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["Art semester 3"] + } + } + }, + { + "id": "urn:epass:learningAssessment:8", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["Art semester 4"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:8", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["Art semester 4"] + } + } + } + ], + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:2", + "type": "LearningAchievementSpecification", + "additionalNote": [ + { + "id": "urn:epass:note:4", + "type": "Note", + "noteLiteral": { + "en": [ + "ISCED 0232: Literature and linguistics. Currently no specific code for native language(?) (Aktuell kein spezifischer code für Muttersprache(?))" + ] + }, + "subject": { + "id": "urn:epass:concept:49166b48-abb0-4d47-a63d-095f0bab0c4d", + "type": "Concept", + "inScheme": { + "id": "urn:epass:conceptScheme:abb415ab-8d9b-4974-8a5c-0e8cdaa22ee1", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["More Information"] + } + } + } + ], + "title": { + "en": ["Art"] + } + } + }, + { + "id": "urn:epass:learningAchievement:3", + "type": "LearningAchievement", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "description": { + "en": [ + "Individual results achieved during the Qualilification phase Q" + ] + }, + "title": { + "en": ["Religion"] + }, + "provenBy": [ + { + "id": "urn:epass:learningAssessment:9", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["Religion semester 1"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:9", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["Religion semester 1"] + } + } + }, + { + "id": "urn:epass:learningAssessment:10", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["Religion semester 2"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:10", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["Religion semester 2"] + } + } + }, + { + "id": "urn:epass:learningAssessment:11", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["Religion semester 4"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:11", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["Religion semester 4"] + } + } + }, + { + "id": "urn:epass:learningAssessment:12", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["Religion semester 3"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:12", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["Religion semester 3"] + } + } + } + ], + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:3", + "type": "LearningAchievementSpecification", + "additionalNote": [ + { + "id": "urn:epass:note:5", + "type": "Note", + "noteLiteral": { + "en": [ + "ISCED 0232: Literature and linguistics. Currently no specific code for native language(?) (Aktuell kein spezifischer code für Muttersprache(?))" + ] + }, + "subject": { + "id": "urn:epass:concept:f2ec5f98-5cbc-4ef2-a166-772070f955c4", + "type": "Concept", + "inScheme": { + "id": "urn:epass:conceptScheme:1e6dba61-3cbd-4d32-80d9-a59a860b56f6", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["More Information"] + } + } + } + ], + "title": { + "en": ["History"] + } + } + }, + { + "id": "urn:epass:learningAchievement:4", + "type": "LearningAchievement", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "description": { + "en": [ + "Individual results achieved during the Qualilification phase Q" + ] + }, + "title": { + "en": ["English"] + }, + "provenBy": [ + { + "id": "urn:epass:learningAssessment:13", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["English semester 1"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:13", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["English semester 1"] + } + } + }, + { + "id": "urn:epass:learningAssessment:14", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["English semester 2"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:14", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["English semester 2"] + } + } + }, + { + "id": "urn:epass:learningAssessment:15", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["English semester 3"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:15", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["English semester 3"] + } + } + }, + { + "id": "urn:epass:learningAssessment:16", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["English semester 4"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:16", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["English semester 4"] + } + } + } + ], + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:4", + "type": "LearningAchievementSpecification", + "additionalNote": [ + { + "id": "urn:epass:note:6", + "type": "Note", + "noteLiteral": { + "en": [ + "ISCED 0232: Literature and linguistics. Currently no specific code for native language(?) (Aktuell kein spezifischer code für Muttersprache(?))" + ] + }, + "subject": { + "id": "urn:epass:concept:7570d85a-516c-42f7-acdf-e372482ef498", + "type": "Concept", + "inScheme": { + "id": "urn:epass:conceptScheme:e9ecb7a8-7078-4748-ad02-a8b7209902db", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["More Information"] + } + } + } + ], + "title": { + "en": ["English"] + } + } + }, + { + "id": "urn:epass:learningAchievement:5", + "type": "LearningAchievement", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "description": { + "en": [ + "Individual results achieved during the Qualilification phase Q" + ] + }, + "title": { + "en": ["German"] + }, + "provenBy": [ + { + "id": "urn:epass:learningAssessment:17", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["German semester 1"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:17", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["German semester 1"] + } + } + }, + { + "id": "urn:epass:learningAssessment:18", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["German semester 2"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:18", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["German semester 2"] + } + } + }, + { + "id": "urn:epass:learningAssessment:19", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["German semester 3"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:19", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["German semester 3"] + } + } + }, + { + "id": "urn:epass:learningAssessment:20", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["German semester 4"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:20", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["German semester 4"] + } + } + } + ], + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:5", + "type": "LearningAchievementSpecification", + "additionalNote": [ + { + "id": "urn:epass:note:7", + "type": "Note", + "noteLiteral": { + "en": [ + "ISCED 0232: Literature and linguistics. Currently no specific code for native language(?) (Aktuell kein spezifischer code für Muttersprache(?))" + ] + }, + "subject": { + "id": "urn:epass:concept:fe5c7749-eb18-4298-9e59-d622e50023cf", + "type": "Concept", + "inScheme": { + "id": "urn:epass:conceptScheme:e0820f5a-4f18-48bd-9352-b61d83456c91", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["More Information"] + } + } + } + ], + "title": { + "en": ["German"] + } + } + } + ], + "provenBy": [ + { + "id": "urn:epass:learningAssessment:23", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["Overall Qualification Q+A"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "hasPart": [ + { + "id": "urn:epass:learningAssessment:21", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": [" Qualification Phase (Q) results"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:21", + "type": "LearningAssessmentSpecification", + "title": { + "en": [" Qualification Phase (Q) results"] + } + } + }, + { + "id": "urn:epass:learningAssessment:22", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["Final Examination results (A)"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["Excellent (5)"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:22", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["Final Examination results (A)"] + } + } + } + ], + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:23", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["Overall Diploma Assessment"] + }, + "gradingScheme": { + "id": "urn:epass:gradingScheme:1", + "type": "GradingScheme", + "description": { + "en": [ + "The Germany national grading scheme for High school consists of six grade levels with\\n numerical equivalents: sehr gut \\u2013 1 (very good); gut \\u2013 2 (good);\\n befriedigend \\u2013 3 (satisfatory); ausrcichend \\u2013 4 (addequate);\\n mangelhalft \\u2013 5 (unsatisfactory- fail); ungenügend \\u2013 6\\n (insufficient- fail). The minimum passing grade is ausrcichend \\u2013\\n 4. /n Equivalents in points: 15, 14, 13 \\u2013 1 (very good); 12, 11,\\n 10 \\u2013 2 (good); 09, 08, 07\\u2013 3 (satisfatory); 06, 05, 04\\u2013 4\\n (addequate); 03, 02, 01\\u2013 5 (unsatisfactory- fail); 0\\u2013 6\\n (insufficient- fail)." + ] + }, + "title": { + "en": ["Grading scheme in Germany"] + } + } + } + } + ], + "specifiedBy": { + "id": "urn:epass:qualification:1", + "type": "Qualification", + "title": { + "en": ["German International Abitur"] + }, + "creditPoint": [ + { + "id": "urn:epass:creditPoint:1", + "type": "CreditPoint", + "framework": { + "id": "http://data.europa.eu/snb/education-credit/6fcec5c5af", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/education-credit/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["European Credit Transfer System"] + } + }, + "point": "0" + } + ], + "maximumDuration": "P21D", + "volumeOfLearning": "P60D", + "nqfLevel": [ + { + "id": "http://data.europa.eu/snb/qdr/c_bd9f8e42", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/qdr/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["DQR Level 5"] + } + } + ], + "eqfLevel": { + "id": "http://data.europa.eu/snb/eqf/4", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/eqf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Level 4"] + } + }, + "isPartialQualification": true + } + } + ] + }, + "issuanceDate": "2024-03-26T16:03:32+01:00", + "issuer": { + "id": "did:ebsi:org:12345689", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:certificateLocation:1", + "type": "Location", + "address": { + "id": "urn:epass:certificateAddress:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/ESP", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { "en": "Spain" } + } + } + } + ], + "identifier": { + "id": "urn:epass:identifier:2", + "type": "Identifier", + "schemeName": "University Aliance ID", + "notation": "73737373" + }, + "legalName": { "en": "ORGANIZACION TEST" } + }, + "issued": "2024-03-26T16:03:32+01:00", + "validFrom": "2020-07-20T00:00:00+02:00", + "credentialProfiles": [ + { + "id": "http://data.europa.eu/snb/credential/e34929035b", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/credential/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Generic"] + } + } + ], + "displayParameter": { + "id": "urn:epass:displayParameter:1", + "type": "DisplayParameter", + "language": [ + { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + } + ], + "description": { + "en": [ + "EBSI Example (not 100% complete) - https://github.com/Knowledge-Innovation-Centre/ESBI-JSON-schemas/blob/main/examples%20of%20credentials/German%20example/Bengales_highSchoolDiploma_v1.json" + ] + }, + "individualDisplay": [ + { + "id": "urn:epass:individualDisplay:f36142e4-9ce0-4aca-b80f-2a3762daee76", + "type": "IndividualDisplay", + "language": { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + }, + "displayDetail": [ + { + "id": "urn:epass:displayDetail:059df8bd-e802-49fc-8e92-2568d0333432", + "type": "DisplayDetail", + "image": { + "id": "urn:epass:mediaObject:77eeb2c5-01c8-4962-9217-e49a108e0a76", + "type": "MediaObject", + "content": "/9j/4AAQSkZJRgABAgAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCARjAxoDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD3+iiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiobq7trG2e5u7iK3gTG6WZwirk4GSeByQKgsdY0zVGddP1G0u2QAuLedZCuemcE4oAu0UUUAFFFFABRRRQAUUUUAFFZjeI9DS7Nq2s6cLkP5ZhN0m8PnG3Gc5zxitOgAooqhe63pOmTLDf6pZWsrLuCT3CRsV6ZwT04P5UAX6KjgnhuoEnt5UlhkG5JI2DKw9QR1qSgAooooAKKzG8R6Gl2bVtZ04XIfyzCbpN4fONuM5znjFadABRRRQAUUUUAFFFFABRRWbdeIdEsbl7a81jT7edMboprlEZcjIyCcjgg0AaVFIrBlDKQVIyCOhpaACiiigAooooAKKKKACiobq7trG2e5u7iK3gTG6WZwirk4GSeByQKgsdY0zVGddP1G0u2QAuLedZCuemcE4oAu0UUUAFFFFABRWZbeI9DvLhLe11nTp53OFjiukZmPsAcmtOgAooooAKKhuru2sbZ7m7uIreBMbpZnCKuTgZJ4HJAqCx1jTNUZ10/UbS7ZAC4t51kK56ZwTigC7RRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRR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igAooooAKKKKACiiigAooooAKKKKACqd9pOm6p5f9oafaXfl52faIVk25xnGQcZwPyq5RQBBaWVrp9uLeytobaBSSI4YwijPXgcVPRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABUF5dxWNnNdTkiOJC7YGScdgO5qeqGtWkt7pFxBAFM2A8YY4DMpDAE9gSMUANhudVd0aXTbeOFiM/6WWkUH1XZtyPZj+NMk1K7mvZ7bTbOKf7OQs0k85iQOQG2jCsScEE8Ac9c5p8OsRTSJF9lv0lYgFXtJAFPfL428eoJHpmsp7G3stRvmvE1TZcTedHJaSXBUgqoIKxHggg9RyMc9cAF6fX1g0SXUHtZA8EqwzW+fmVt4U4Iznrkeox0zVXW7zWE0h5VtIID58AXF4wfBkUEHCYHYEAkYJ545Lmzi/sFxY212POu4ZWE3mNI2JY8sd5LY2r36AVoa9BLcaPKkEZkkVo5Qi9W2OrED3IWgBt7dTQ6X5uoWVux+0RIIo5i68yIA2Sg5BOcY7Dn0o+IBdXGqaXZrZ2tzayO7NHPMVDsEPDDYwIGc9+cccZqzqk39paNm2huCRdW/yvA6NxKhJ2sAcAc56cH0qa/ikfWtJkWNmRGl3sBkLlCBk9qAItOmkXVpLOfTrS2kitYyj28pcbNzALyi4AwfzpRql9dNM+nafFPbxO0fmS3PlmRlJDbAFbIBBGSRyPTmpEikHieaby28o2cah8cEh3OM+vIrHtrG001JLW8j1gOssjK1tJdNG6sxYEeWSFODyDjnPbmgDVfWhJa2L2duZZr0lY45X8sIQCW3nBxjBHAPP51etJLx1b7ZbwwuD8ohmMgI+pVf5VmyQ6bbaTbwy2F2bYsXUCKSWSNySdxK5cMSTz707RWma4uwn2z+zwEEBvFYSbvm343/Pt+5jdznd2xQA3X7i+guNJFkkbeZd7XDztGG/duQDhTkcZ/AcekWt3FxEuiyzWwNx9vA8mGTeCTHIBhiF9skgY5q3rccm2xuUieVbW6WWRY13Nt2spIA5ON2cDnimX7G9k0eeCKYot7ubdEyFR5cgyQQCBkjr6j1oAlg1C6XUI7O/tIoHmRmheGcyK23GQcqpB5z0PAPNPuJ9TWd1tbC3kiXGHmujGW4zwAjfTkjp+NR38Uj61pMixsyI0u9gMhcoQMntWbLFb/wBp3/8Aa2mz3rtIDa5tWmj8vYuAvBVDu3Zzj1zjFAEmr6ncXHhpLzT0KSNcRI6ySmNkImCsuVB53Aqe2M9eh2rVrp4ibuGGKTPCxSmQY9clV/lXN2en3ieCXtjaeXcJdyzC3jXaMC6aTaoOOCBx2ORXSWt1HeRGSNZlUHGJoXiP5MAfxoAnooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAopCcAk549Bmuan+IHhu21D+z57y4jvMgeQ1jOHOeny7M1UYSl8KuTKUY/E7HTUVgp408PtdJbSX5tpn4VbuCS33fTzFWt7rRKMo/ErDUk9mFFFFSMKKKKACiqGsa3p2gWQvNUultoC4jDsCcsegwAT2P5VeVldFdGDKwyCDkEU7O1xXV7C0VHcTpbQPM6yMqDJEcbOx+iqCT+ArJ8P8AijTfE32o6aZmW1cJI0kRT5jngZ54xz9aajJpyS0QOSTt1NqiisrUvEmkaTcpa3l4q3MgysEaNJIR67EBOPwpKLk7JA2lqzVorGtfFmhXt5DZW+oxtdzMVS3KsJMhSxyhGVGATkgVs0Si47qwKSezCiiikMKKq6lqVppGnzX9/OsFrCMySMCcc46Dk8kU+zvLfULOG7tJVlt5kDxuvRgehp2dr9BXV7E9FFYNr4v0q98QnQ4PtRvlQu6SWzx7FAzk7gD3H501GUr2WwOSW5vUUUVIwooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACvJfDijxF8bdY1XG6DTlaND23ACIfnhzXpeuakukaFf6i2MW0DyAHuQOB+JwK4X4Maa0Hhi61SXJlv7kncerKnA/8eL11UfcpTn8vv3Oer71SMPmdxrui2mv6PcadeRq8cqEAkZKN2YehBrhPg1r11qOi3ulXcjSNpzqImY5IRs4X8Cp/MV3HiPWItB8O32pysB5ERKZ/ifoo/EkCvMfhp/xSfgLWvFN7Gdk2PJQ8bwmQv5u+PwqqUXLDyT7q3qTUdq0Wuzv6Hqt9q9jp0kUVzPiaXPlworSSPjrhFBY/gKrW/ibSLrU002K7zevG0nkGJ1ZVBIO4EfL06HBrD+H2nTtpR8Rao3m6tqw855G/giP3EX0XGDj39qyvA7Ra58QvFXiKLDQhktIHHRgAASP++FP41Hsorm/u/mX7ST5fP8jsrLxJpGoarLpdpepLexJ5jxBWBVcgZyRjuPzqWbXNNt9at9Hmu0TULiMyRQkHLKM85xjseM9jXG6qPsPxt0SdRtW+sJIXI7lQ5/ov5V0M1jFq/jCzvvKUx6QkiibHLyuANoPoq5J92HcGlKnBWfRq/wA/+HGpyd11Tt/XyK/iHVfCt7d2/h/W8TzzTqIrd4JOXzgEEDHfrnHWunASKMABURR06AAV504/t743ov3oNEs8n08xh/P5x/3zUvia+m8TeNrbwXbSMljGn2jVGQ4LpwRHkdAcrn/eHpVOjflin0u/IlVbXlbrZeZ0Go+MtJtdKu7uC580QxSNHIsTmKR1UnasmNhPHQGsz4Vaa1h4FtppAfOvpHunJ6nJwP8Ax1Qfxqh8VpIYvClloFqiLNf3MUFvCowAqkdB6A7R+Nd7Y2kdhYW1nCMRW8SxJ9FAA/lSlaNHT7T/AC/4ccbyq69F+ZPXk/g7Vo9M+KXiSy1vMWoX0+LaSXoVDMVQE9ipTHrtA9K9TnuYbYRmZwgkdY1z3YnAFcl4/wDA0PivT/tFtiLV7dc28o43452MfT0PY/jSw8oq8J7S0uFaMnaUd10Ni70GGbxZYa8RGrWttNE7H7xLFdv4AeZ+dPsvFGialqbadY6lDc3SoXZISWAUYySRx3HesD4YeJbzxD4addRJa8spfIkkbq4wCCffsfpnvXN+CYm8U+MfFGqMG+xTTiJpBx5kQJxGD6EBS3sMfxcX7F+8qj+H/Mn2vwuH2j0Kw8VaHqmqy6ZY6lDPeRKWaNMngHBw2MH8DRD4r0K51w6LDqUMmoDP7lcnkdRnGMjB4zng1wOmwNr/AMWNd+ygw2llAtkZIvl8tBgMqkdGJVgPQFu4FXbS1h1D4z7LaJI7TQdPEaLGuFDsOB+Uh/75puhBX32uCrTdvWx0virXfDVnaPp3iF8w3ICmEwSMJO4AKjrnHeptY1jTvBnh9XFnObeCPbFBbQlsADueij3J/OuW8ZD+3PiX4X0IfNFbE3s47YByAf8Avgj/AIFV34r3kkXg8afBzcalcx2yKOpydx/9BA/GlGkm6cX11YSqNc8uxoeBvEc/iHSPtN6JUupWaUR/ZnSOOMnCKrlQr8AHIJ61g+Ebm3uvGPi/xVdzRxWsUos45nYBQiYDHJ/3UP411uozReFvBc7xEBNPstsfuVXCj8TiuT+GWjqPB9ne6gmIFd7iOOTozknMzD1AAC+mCe4w1y8k5rRPT9RPm5oxerWv6HZ6L4j0jxEkz6TepciBgsm0EFSenBA9Dz7VqV538KY2vIdd8Quu06pfsyDH8Ckkfq5H4V6JWFaChUcV0NqU3OCkwooorI0CiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigDi/iRaa7rHh6TR9F0uW4NwymWbzokUKDnHzMDnIHbGKXwzJrWh+GLDSz4Vu/OtogjH7Vb7GbqTkPnkknpXZ0Vsq3uezsrb9f8zL2Xv8APfU4a/8ACWreL72CTxNcQ2+mQPvj0yzctvPrJIQMntwOnTFaXjPw1Jrfgq40bTRFC4VPIj+6nyEEL7DAx+VdPRS9tO6a6bD9lGzT6nH6LB4ku9Dt9L1PTYNNiggWGR1uRI84UYwoXhAccnJOCQPUVfhv4e1rQdIa21KGK0/fvM6I6u0zMAATjhVAA4ByT6Ywe6opus2nGysxKkk077HHeKfDOpaz4t0DULG4W2jshN50/BZQcABQepPzc9B+h6y2torO2jt4F2xoMAZyfqT3J6k96loqJVJSiovoWoJNtdTgfDHh7XrDxR4guruCOCG/vPNF0JQzPErMVRV7ZyMk446DPIhOheIdC+I+o67p2mRanaahEEINysTRH5eue2V7A8H2r0SitPrErttLVWM/YxslfZ3POdc8MeIb7xloOtG3trz7NuaVPP8ALihb+EDIJIBwcgZJz0GAPQbaOWK3RJ5vOlHLvt2gk88DsPT27nrUtFROq5pJ9C4U1Ftrqcz400nWNYsbCHRZYYbiG8S5MszEKuwMQCACTltvam3GqeLDZmCD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UUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAFFFFABRRRQAUUUUAf/9k=", + "contentEncoding": { + "id": "http://data.europa.eu/snb/encoding/6146cde7dd", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/encoding/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["base64"] + } + }, + "contentType": { + "id": "http://publications.europa.eu/resource/authority/file-type/JPEG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/file-type", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["JPEG"] + }, + "notation": "file-type" + } + }, + "page": 1 + } + ] + } + ], + "primaryLanguage": { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + }, + "title": { + "en": ["Bengales_highSchoolDiploma"] + } + } +} From 98b7270486f534571a43671f9a049b2ab26f525a Mon Sep 17 00:00:00 2001 From: Ronald Date: Wed, 8 Jan 2025 09:42:04 +0100 Subject: [PATCH 28/45] small test image --- test/edubadges_100x100.png | Bin 0 -> 12756 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/edubadges_100x100.png diff --git a/test/edubadges_100x100.png b/test/edubadges_100x100.png new file mode 100644 index 0000000000000000000000000000000000000000..f4ca56983bdba1dd4273045612a6890e5dbe741d GIT binary patch literal 12756 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 m#k Date: Thu, 9 Jan 2025 00:22:27 +0100 Subject: [PATCH 29/45] add image base 64 encodig and EDCL support --- .../examples/DigiComp_Generic_test.json | 615 ++++++++++++++++++ .../DigiComp_Generic_test_smaller.jsonld | 151 +++++ .../custom_mapping_OBv3_ELM_latest.json | 82 ++- .../custom_mapping_OBv3_ELM_org_250108.json | 235 +++++++ .../Complete_OpenBadgeCredential_image.json | 2 +- outputs/translated_DigiComp_Generic.json | 67 ++ src/backend/base64_encode.rs | 390 ++++++++++- src/backend/init_conversion.rs | 23 + src/backend/repository.rs | 117 +--- src/backend/routes/root.rs | 3 + src/backend/routes/translate_file.rs | 5 +- uploads/DigiComp_Generic.json | 612 +++++++++++++++++ 12 files changed, 2156 insertions(+), 146 deletions(-) create mode 100644 json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic_test.json create mode 100644 json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic_test_smaller.jsonld create mode 100644 json/mapping/custom_mapping_OBv3_ELM_org_250108.json create mode 100755 outputs/translated_DigiComp_Generic.json create mode 100755 uploads/DigiComp_Generic.json diff --git a/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic_test.json b/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic_test.json new file mode 100644 index 0000000..8a499fe --- /dev/null +++ b/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic_test.json @@ -0,0 +1,615 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "http://data.europa.eu/snb/model/context/edc-ap" + ], + "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", + "type": [ + "VerifiableCredential", + "VerifiableAttestation", + "EuropeanDigitalCredential" + ], + "credentialSchema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialSubject": { + "id": "did:key:afsdlkj34134", + "type": "Person", + "identifier": [ + { + "id": "urn:epass:identifier:2", + "type": "Identifier", + "notation": "545465468", + "schemeName": "Student ID" + } + ], + "givenName": { + "en": ["David"] + }, + "familyName": { + "en": ["Smith"] + }, + "fullName": { + "en": ["David Smith"] + }, + "hasClaim": [ + { + "id": "urn:epass:learningAchievement:2", + "type": "LearningAchievement", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["TITLE OF PROGRAMME"] + }, + "hasPart": [ + { + "id": "urn:epass:learningAchievement:1", + "type": "LearningAchievement", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["Topic #1"] + }, + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "type": "LearningAchievementSpecification", + "title": { + "en": ["Topic #1"] + }, + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "type": "LearningOutcome", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "type": "Concept", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["5.4 Identifying digital competence gaps"] + } + } + ], + "title": { + "en": ["Name of DigiComp Competence"] + } + } + ] + } + }, + { + "id": "urn:epass:learningAchievement:1", + "type": "LearningAchievement", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["Topic #1"] + }, + "specifiedBy": { + "id": "urn:epass:learningAchievementSpec:1", + "type": "LearningAchievementSpecification", + "title": { + "en": ["Topic #1"] + }, + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "type": "LearningOutcome", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/860966ekgo", + "type": "Concept", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["5.4 Identifying digital competence gaps"] + } + } + ], + "title": { + "en": ["Name of DigiComp Competence"] + } + } + ] + } + } + ], + "provenBy": [ + { + "id": "urn:epass:learningAssessment:1", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:location:1", + "type": "Location", + "address": [ + { + "id": "urn:epass:address:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + }, + "fullAddress": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["Here"] + } + } + } + ], + "description": { + "en": ["The Address"] + } + } + ], + "legalName": { + "en": ["University of Life"] + }, + "registration": { + "id": "urn:epass:legalIdentifier:2", + "type": "LegalIdentifier", + "notation": "987654321", + "spatial": { + "id": "http://publications.europa.eu/resource/authority/country/BEL", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Belgium"] + }, + "notation": "country" + } + } + } + ] + }, + "title": { + "en": ["Overall Diploma Assessment"] + }, + "grade": { + "id": "urn:epass:note:2", + "type": "Note", + "noteLiteral": { + "en": ["10"] + } + }, + "specifiedBy": { + "id": "urn:epass:learningAssessmentSpec:1", + "type": "LearningAssessmentSpecification", + "title": { + "en": ["Overall Diploma Assessment"] + } + } + } + ], + "specifiedBy": { + "id": "urn:epass:qualification:1", + "type": "Qualification", + "title": { + "en": ["Title of Achievement"] + }, + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:2", + "type": "LearningOutcome", + "relatedSkill": [ + { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", + "type": "Concept", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["5.4 Identifying digital competence gaps"] + } + } + ], + "title": { + "en": ["Name of DigiComp Competence"] + } + }, + { + "id": "urn:epass:learningOutcome:3", + "type": "LearningOutcome", + "relatedSkill": [ + { + "id": "http://data.europa.eu/snb/dcf/34v10n662m", + "type": "Concept", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["3.1 Proficiency Level Foundation 2"] + } + } + ], + "title": { + "en": ["Name of DigiComp Competence 2"] + } + } + ], + "learningOutcomeSummary": { + "id": "urn:epass:note:3", + "type": "Note", + "noteLiteral": { + "en": [ + "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" + ] + } + }, + "eqfLevel": { + "id": "http://data.europa.eu/snb/eqf/5", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/eqf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Level 5"] + } + } + } + } + ] + }, + "issuer": { + "id": "did:ebsi:org:12345689", + "type": "Organisation", + "location": [ + { + "id": "urn:epass:certificateLocation:1", + "type": "Location", + "address": { + "id": "urn:epass:certificateAddress:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/ESP", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { "en": "Spain" } + } + } + } + ], + "identifier": { + "id": "urn:epass:identifier:2", + "type": "Identifier", + "schemeName": "University Aliance ID", + "notation": "73737373" + }, + "legalName": { "en": "ORGANIZACION TEST" } + }, + "issuanceDate": "2024-03-26T16:06:50+01:00", + "issued": "2024-03-26T16:06:50+01:00", + "validFrom": "2019-09-20T00:00:00+02:00", + "credentialProfiles": [ + { + "id": "http://data.europa.eu/snb/credential/e34929035b", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/credential/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Generic"] + } + } + ], + "displayParameter": { + "id": "urn:epass:displayParameter:1", + "type": "DisplayParameter", + "language": [ + { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + } + ], + "description": { + "en": [ + "EBSI Example https://github.com/Knowledge-Innovation-Centre/ESBI-JSON-schemas/blob/main/examples%20of%20credentials/DigiComp%20Generic.json" + ] + }, + "individualDisplay": [ + { + "id": "urn:epass:individualDisplay:c05743e7-9f9d-4e0b-899b-7ae6514c7a02", + "type": "IndividualDisplay", + "language": { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + }, + "displayDetail": [ + { + "id": "urn:epass:displayDetail:2804bbf5-ab29-4972-9202-71af0f85316b", + "type": "DisplayDetail", + "image": { + "content": "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", + "contentEncoding": { + "id": "http://data.europa.eu/snb/encoding/6146cde7dd", + "inScheme": { + "id": "http://data.europa.eu/snb/encoding/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "base64" + ] + }, + "type": "Concept" + }, + "contentType": { + "id": "http://publications.europa.eu/resource/authority/file-type/PNG", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/file-type", + "type": "ConceptScheme" + }, + "notation": "file-type", + "prefLabel": { + "en": [ + "PNG" + ] + }, + "type": "Concept" + }, + "id": "urn:epass:mediaObject:https://avatars.githubusercontent.com/u/22613412?v=4", + "type": "MediaObject" + } + } + ] + } + ], + "primaryLanguage": { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + }, + "title": { + "en": ["DigiComp Generic"] + } + } +} diff --git a/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic_test_smaller.jsonld b/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic_test_smaller.jsonld new file mode 100644 index 0000000..2cf81e9 --- /dev/null +++ b/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic_test_smaller.jsonld @@ -0,0 +1,151 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "http://data.europa.eu/snb/model/context/edc-ap" + ], + "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", + "type": [ + "VerifiableCredential", + "VerifiableAttestation", + "EuropeanDigitalCredential" + ], + "credentialSchema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialSubject": { + "id": "did:key:afsdlkj34134", + "type": "Person", + "identifier": [ + { + "id": "urn:epass:identifier:2", + "type": "Identifier", + "notation": "545465468", + "schemeName": "Student ID" + } + ], + + "fullName": { + "en": ["David Smith"] + }, + "hasClaim": [ + { + "id": "urn:epass:learningAchievement:2", + "type": "LearningAchievement", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + + "legalName": { + "en": ["University of Life"] + } + } + ] + }, + "title": { + "en": ["TITLE OF PROGRAMME"] + } + } + ] + }, + "issuer": { + "id": "did:ebsi:org:12345689", + "type": "Organisation", + "identifier": { + "id": "urn:epass:identifier:2", + "type": "Identifier", + "schemeName": "University Aliance ID", + "notation": "73737373" + }, + "legalName": { "en": "ORGANIZACION TEST" } + }, + "issuanceDate": "2024-03-26T16:06:50+01:00", + "issued": "2024-03-26T16:06:50+01:00", + "validFrom": "2019-09-20T00:00:00+02:00", + "credentialProfiles": [ + { + "id": "http://data.europa.eu/snb/credential/e34929035b", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/credential/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Generic"] + } + } + ], + "displayParameter": { + "id": "urn:epass:displayParameter:1", + "type": "DisplayParameter", + "language": [ + { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + } + ], + "description": { + "en": [ + "EBSI Example https://github.com/Knowledge-Innovation-Centre/ESBI-JSON-schemas/blob/main/examples%20of%20credentials/DigiComp%20Generic.json" + ] + }, + "individualDisplay": [ + { + "id": "urn:epass:individualDisplay:c05743e7-9f9d-4e0b-899b-7ae6514c7a02", + "type": "IndividualDisplay", + "language": { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + }, + "displayDetail": [ + { + "id": "urn:epass:displayDetail:2804bbf5-ab29-4972-9202-71af0f85316b", + "type": "DisplayDetail", + "image": {"content": "1"} + } + ] + } + ], + "primaryLanguage": { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + }, + "title": { + "en": ["DigiComp Generic"] + } + } +} diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index 589b00e..c19896a 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -53,6 +53,17 @@ "path": "$.issuer.legalName.en" } }, + { + "type_": "stringit", + "source": { + "value": "Organisation" + }, + "destination": { + "format": "ELM", + "path": "$.issuer.type" + } + }, + { "type_": "copy", "source": { @@ -108,80 +119,109 @@ } }, { - "type_": "markdownToJson", + "type_": "imageToIndividualDisplay", "source": { "format": "OBv3", - "path": "$.credentialSubject.achievement.criteria.narrative" + "path": "$.image" }, "destination": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome[0]" + "path": "$.displayParameter" + } + }, + { + "type_": "stringit", + "source": { + "value": "LearningAchievement" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].type" } }, { - "type_": "imageToIndividualDisplay", + "type_": "stringit", "source": { - "format": "OBv3", - "path": "$.image" + "value": "urn:epass:awardingProcess:1" }, "destination": { "format": "ELM", - "path": "$.displayParameter" + "path": "$.credentialSubject.hasClaim[0].awardedBy.id" } - }, + }, + { + "type_": "stringit", + "source": { + "value": "AwardingProcess" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].awardedBy.type" + } + }, { "type_": "copy", "source": { "format": "OBv3", - "path": "$.name" + "path": "$.issuer.id" }, "destination": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].title.en[0]" + "path": "$.credentialSubject.hasClaim[0].awardedBy.awardingBody[0].id" + } + }, + { + "type_": "stringit", + "source": { + "value": "Organisation" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].awardedBy.awardingBody[0].type" } }, { "type_": "copy", "source": { "format": "OBv3", - "path": "$.credentialSubject.achievement.id" + "path": "$.issuer.name" }, "destination": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].id" + "path": "$.credentialSubject.hasClaim[0].awardedBy.awardingBody[0].legalName.en[0]" } }, { "type_": "copy", "source": { "format": "OBv3", - "path": "$.credentialSubject.achievement.name" - }, + "path": "$.name" + }, "destination": { "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].specifiedBy.title.en[0]" + "path": "$.credentialSubject.hasClaim[0].title.en[0]" } }, { "type_": "copy", "source": { "format": "OBv3", - "path": "$.credentialSchema" + "path": "$.credentialSubject.achievement.id" }, "destination": { "format": "ELM", - "path": "$.credentialSchema" + "path": "$.credentialSubject.hasClaim[0].id" } }, { "type_": "copy", "source": { "format": "OBv3", - "path": "$.credentialStatus" + "path": "$.credentialSchema" }, "destination": { "format": "ELM", - "path": "$.credentialStatus" + "path": "$.credentialSchema" } }, { @@ -205,7 +245,7 @@ }, "destination": { "format": "ELM", - "path": "$.credentialSubject.givenName.en[0]" + "path": "$.credentialSubject.fullName.en[0]" } }, { @@ -217,7 +257,7 @@ }, "destination": { "format": "ELM", - "path": "$.credentialSubject.givenName" + "path": "$.credentialSubject.fullName" } }, { diff --git a/json/mapping/custom_mapping_OBv3_ELM_org_250108.json b/json/mapping/custom_mapping_OBv3_ELM_org_250108.json new file mode 100644 index 0000000..589b00e --- /dev/null +++ b/json/mapping/custom_mapping_OBv3_ELM_org_250108.json @@ -0,0 +1,235 @@ +[ + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.@context" + }, + "destination": { + "format": "ELM", + "path": "$.@context" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.id" + }, + "destination": { + "format": "ELM", + "path": "$.id" + } + }, + { + "type_": "stringArrayIt", + "source": { + "value": ["VerifiableCredential", "VerifiableAttestation", "EuropeanDigitalCredential"] + }, + "destination": { + "format": "ELM", + "path": "$.type" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.issuer.id" + }, + "destination": { + "format": "ELM", + "path": "$.issuer.id" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.issuer.name" + }, + "destination": { + "format": "ELM", + "path": "$.issuer.legalName.en" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.validFrom" + }, + "destination": { + "format": "ELM", + "path": "$.validFrom" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.validFrom" + }, + "destination": { + "format": "ELM", + "path": "$.issuanceDate" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.validFrom" + }, + "destination": { + "format": "ELM", + "path": "$.issued" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.id" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.id" + } + }, + { + "type_": "stringit", + "source": { + "value": "Person" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.type" + } + }, + { + "type_": "markdownToJson", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.criteria.narrative" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome[0]" + } + }, + { + "type_": "imageToIndividualDisplay", + "source": { + "format": "OBv3", + "path": "$.image" + }, + "destination": { + "format": "ELM", + "path": "$.displayParameter" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.name" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].title.en[0]" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.id" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].id" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.name" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.title.en[0]" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSchema" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSchema" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialStatus" + }, + "destination": { + "format": "ELM", + "path": "$.credentialStatus" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "Student ID", + "path": "$.credentialSubject.identifier[0]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.identifier" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "ext:givenName", + "path": "$.credentialSubject.identifier[1]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.givenName.en[0]" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "ext:familyName", + "path": "$.credentialSubject.identifier[2]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.givenName" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "ext:fullName", + "path": "$.credentialSubject.identifier[3]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.fullName" + } + } +] \ No newline at end of file diff --git a/json/obv3/examples/Complete_OpenBadgeCredential_image.json b/json/obv3/examples/Complete_OpenBadgeCredential_image.json index 87dcf8c..91a285d 100644 --- a/json/obv3/examples/Complete_OpenBadgeCredential_image.json +++ b/json/obv3/examples/Complete_OpenBadgeCredential_image.json @@ -12,7 +12,7 @@ "name": "1EdTech University Degree for Example Student", "description": "1EdTech University Degree Description", "image": { - "id": "https://avatars.githubusercontent.com/u/22613412?v=4", + "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/edubadges_100x100.png", "type": "Image", "caption": "1EdTech University Degree for Example Student" }, diff --git a/outputs/translated_DigiComp_Generic.json b/outputs/translated_DigiComp_Generic.json new file mode 100755 index 0000000..c53be1b --- /dev/null +++ b/outputs/translated_DigiComp_Generic.json @@ -0,0 +1,67 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://accrediter.edu/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialSubject": { + "achievement": { + "criteria": { + "narrative": "**id**:\n urn:epass:learningOutcome:2\n**relatedSkill**:\n - **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo\n **inScheme**:\n **id**:\n https://publications.europa.eu/resource/authority/snb/dcf/25831c2\n **type**:\n ConceptScheme\n **prefLabel**:\n **en**:\n - 5.4 Identifying digital competence gaps\n **type**:\n Concept\n**title**:\n **en**:\n - Name of DigiComp Competence\n**type**:\n LearningOutcome\n" + }, + "id": "urn:epass:learningAchievement:2", + "name": "Title of Achievement", + "type": [ + "Achievement" + ] + }, + "familyName": "Smith", + "fullName": "David Smith", + "givenName": "David", + "id": "did:key:afsdlkj34134", + "identifier": [ + { + "hashed": false, + "identityHash": "545465468", + "identityType": "ext:studentID", + "salt": "not-used", + "type": "IdentityObject" + } + ], + "type": [ + "AchievementSubject", + "profile" + ] + }, + "id": "urn:credential:dffc6c22-1421-4df2-b0e4-2b9d17aa0a6b", + "issuer": { + "address": { + "addressCountryCode": "http://publications.europa.eu/resource/authority/country/ESP" + }, + "id": "did:ebsi:org:12345689", + "name": "ORGANIZACION TEST", + "type": [ + "Profile" + ] + }, + "name": "TITLE OF PROGRAMME", + "image": { + "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/edubadges.png", + "type": "Image", + "caption": "1EdTech University Degree for Example Student" + }, + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "validFrom": "2019-09-20T00:00:00+02:00" +} \ No newline at end of file diff --git a/src/backend/base64_encode.rs b/src/backend/base64_encode.rs index 9eaef61..b700e92 100644 --- a/src/backend/base64_encode.rs +++ b/src/backend/base64_encode.rs @@ -1,11 +1,9 @@ -use num_traits::ops::bytes; -//use reqwest::Client; -//use reqwest::blocking::get; use base64::{engine::general_purpose::STANDARD as Base64Engine, Engine}; +//use serde::Serialize; use std::error::Error; -use std::io::{BufReader, Read, copy}; -use std::fs::File; +use std::io::Read; use ureq::get; +use serde_json::{json, Map, Value}; /// Fetches an image from the given URL, encodes it in Base64, and returns the encoded string. /// @@ -15,9 +13,7 @@ use ureq::get; /// # Returns /// - `Ok(String)`: The Base64-encoded string of the image if successful. /// - `Err(Box)`: An error if the fetch or encoding fails. -pub fn encode_image_from_url(url: &str) -> Result> { - - let filename = "downloaded_image.jpg"; +fn encode_image_from_url(url: &str) -> Result> { let resp = get(url).call().expect("Failed to download image"); assert!(resp.has("Content-Length")); @@ -30,19 +26,379 @@ pub fn encode_image_from_url(url: &str) -> Result> { .take(10_000_000) .read_to_end(&mut bytes)?; - // let mut file = File::create(filename).expect("Failed to create file"); - // copy(&mut resp.into_reader(), &mut file).expect("Failed to save image"); - - // let mut reader = BufReader::new(file); - // let mut image_bytes : Vec = Vec::new(); - // reader.read_to_end(&mut image_bytes).unwrap(); - // println!("{:#?}", image_bytes); - - // Encode the image bytes as a Base64 string let base64_string = Base64Engine.encode(&bytes); Ok(base64_string) } +/// Creates contentType object based on input type in string +/// +/// # Arguments +/// - `content_type`: The a type that is than rewritten into a serde Value object. +/// +/// # Returns +/// - `Ok(Value)`: The content value Object in ELM format if successful. +/// - `Err(Box)`: An error if the fetch or encoding fails. +fn set_content_type(content_type: &str ) -> Result> { + let content_json = r#" + { + "id": "http://publications.europa.eu/resource/authority/file-type/PNG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/file-type", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["PNG"] + }, + "notation": "file-type" + } + "#; + + let mut parsed_contentType_json: Value = serde_json::from_str(content_json).unwrap(); + + match content_type { + "PNG" => { + parsed_contentType_json["id"] = Value::String("http://publications.europa.eu/resource/authority/file-type/PNG".to_string()); + parsed_contentType_json["prefLabel"]["en"][0] = Value::String("PNG".to_string()); + } + "JPG" => { + parsed_contentType_json["id"] = Value::String("http://publications.europa.eu/resource/authority/file-type/JPG".to_string()); + parsed_contentType_json["prefLabel"]["en"][0] = Value::String("JPG".to_string()); + } + "SVG" => { + parsed_contentType_json["id"] = Value::String("http://publications.europa.eu/resource/authority/file-type/SVG".to_string()); + parsed_contentType_json["prefLabel"]["en"][0] = Value::String("SVG".to_string()); + } + _ => { + // Handle other cases + return Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidInput,"This file Type is not exporteswent wrong!"))); + } + } + Ok(parsed_contentType_json) + } + + +/// Creates contentEncodingType object based on input type in string +/// +/// # Arguments +/// - `content_encoding_type`: The a type that is than rewritten into a serde Value object. +/// +/// # Returns +/// - `Ok(Value)`: The content value Object in ELM format if successful. +/// - `Err(Box)`: An error if the fetch or encoding fails. +fn set_content_enconding_type(content_encoding_type: &str ) -> Result> { + let template_json = r#" + { + "id": "http://data.europa.eu/snb/encoding/6146cde7dd", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/encoding/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["base64"] + } + } + "#; + + let mut parsed_content_encoding_type_json: Value = serde_json::from_str(template_json).unwrap(); + + match content_encoding_type { + "base64" => { + parsed_content_encoding_type_json["id"] = Value::String("http://data.europa.eu/snb/encoding/6146cde7dd".to_string()); + parsed_content_encoding_type_json["prefLabel"]["en"][0] = Value::String("base64".to_string()); + } + _ => { + // Handle other cases + return Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidInput,"This content encoding Type is not supported!"))); + } + } + Ok(parsed_content_encoding_type_json) + } + + +/// Creates language object based on input type in string +/// +/// # Arguments +/// - `content_encoding_type`: The a type that is than rewritten into a serde Value object. +/// +/// # Returns +/// - `Ok(Value)`: The content value Object in ELM format if successful. +/// - `Err(Box)`: An error if the fetch or encoding fails. +fn set_language(language: &str ) -> Result> { + let template_json = r#" + { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + } + "#; + + let mut parsed_language_json: Value = serde_json::from_str(template_json).unwrap(); + + match language { + "ENG" => { + parsed_language_json["id"] = Value::String("http://publications.europa.eu/resource/authority/language/ENG".to_string()); + parsed_language_json["prefLabel"]["en"][0] = Value::String("English".to_string()); + } + "NLD"=> { + parsed_language_json["id"] = Value::String("http://publications.europa.eu/resource/authority/language/NLD".to_string()); + parsed_language_json["prefLabel"]["en"][0] = Value::String("dutch".to_string()); + } + _ => { + // Handle other cases + return Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidInput,"This language Type is not supported!"))); + } + } + Ok(parsed_language_json) + } + + + + + + +pub fn image_to_individual_display(image_value: Value) -> Value { + //inspect the image object and re write it so it can be reused in ELM + + //we need to achieve the following structure into the indivudualDisplay array: + let json_data = r#" + { + "id": "urn:epass:individualDisplay:c05743e7-9f9d-4e0b-899b-7ae6514c7a02", + "type": "IndividualDisplay", + "language": { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + }, + "displayDetail": [ + { + "id": "urn:epass:displayDetail:123", + "type": "DisplayDetail", + "image": { + "id": "urn:epass:mediaObject:https://avatars.githubusercontent.com/u/22613412?v=4", + "type": "MediaObject", + "content": "bas64content", + "contentEncoding": { + "id": "http://data.europa.eu/snb/encoding/6146cde7dd", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/encoding/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["base64"] + } + }, + "contentType": { + "id": "http://publications.europa.eu/resource/authority/file-type/JPG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/file-type", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["JPG"] + }, + "notation": "file-type" + } + } + } + ] + } + "#; + + + let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); + + // Directly mutate the `content` value + // first try to encode the image in the URL: + let encoded_string = match encode_image_from_url("https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/edubadges.png") { + Ok(encoded_string) => { + println!("Successfully encoded the image."); + encoded_string // Assign the encoded string to the variable + } + Err(e) => { + eprintln!("Error: {}", e); + String::new() // Assign an empty string or a default value in case of an error + } + }; + + if let Some(image_content) = parsed_json["displayDetail"][0]["image"]["content"].as_str() { + parsed_json["displayDetail"][0]["image"]["content"] = Value::String(encoded_string); + } else { + println!("Key 'content' in 'image' not found."); + } + + + // Directly mutate the `contentType` value + // Set the contentType to a choosen value (currently default to PNG) + let encoded_content_type = match set_content_type("PNG") { + Ok(encoded_content_type) => { + println!("Successfully added the contentType."); + encoded_content_type // Assign the encoded string to the variable + } + Err(e) => { + eprintln!("Error: {}", e); + Value::Null // Assign an empty string or a default value in case of an error + } + }; + + if let Some(_content_type) = parsed_json["displayDetail"][0]["image"].as_object() { + parsed_json["displayDetail"][0]["image"]["contentType"] = encoded_content_type; + } else { + println!("Key 'contentType' in 'image' not found."); + } + + + // Directly mutate the `encoding` value + // first try to encode the image in the URL: + let encoding_value = match set_content_enconding_type("base64") { + Ok(encoding_value) => { + println!("Successfully added encoding type to the image."); + encoding_value // Assign the encoded string to the variable + } + Err(e) => { + eprintln!("Error: {}", e); + Value::Null // Assign an empty string or a default value in case of an error + } + }; + + if let Some(image_encoding) = parsed_json["displayDetail"][0]["image"]["contentEncoding"].as_object() { + parsed_json["displayDetail"][0]["image"]["contentEncoding"] = encoding_value; + } else { + println!("Key 'contentEncoding' in 'image' not found."); + } + + + // Directly mutate the `language` value + // first try to encode the image in the URL: + let language_value = match set_language("ENG") { + Ok(language_value) => { + println!("Successfully added language to the individual display properties."); + language_value // Assign the encoded string to the variable + } + Err(e) => { + eprintln!("Error: {}", e); + Value::Null // Assign an empty string or a default value in case of an error + } + }; + + if let Some(_language) = parsed_json["language"].as_object() { + parsed_json["language"] = language_value; + } else { + println!("Key 'language' in 'individualDisplay' not found."); + } + + + + //println!("{:#?}", parsed_json); + parsed_json + + // if let Some(id_value) = identity_value.get("identityHash") { + // if identity_type.eq(&"Student ID".to_string()) { + // let mut new_object = Map::new(); + // new_object.insert("id".to_string(), Value::String("urn:epass:identifier:2".to_string())); + // new_object.insert("type".to_string(), Value::String("Identifier".to_string())); + // new_object.insert("notation".to_string(), id_value.clone()); + // new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); + // let _current_value = Value::Object(new_object); + // _current_value + // } else { + // id_value.clone() + // } + // } else { + // Value::String("".to_string()) + // } +} + + +pub fn create_display_parameter(image_value: Value) -> Value { + //inspect the image object and re write it so it can be reused in ELM + + //we need to achieve the following structure into the indivudualDisplay array: + let json_data = r#" + { + "id": "urn:epass:displayParameter:1", + "type": "DisplayParameter", + "language": [ + { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + } + ], + "description": { + "en": [ + "EBSI Example https://github.com/Knowledge-Innovation-Centre/ESBI-JSON-schemas/blob/main/examples%20of%20credentials/DigiComp%20Generic.json" + ] + }, + "individualDisplay": [], + "primaryLanguage": { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + }, + "title": { + "en": ["DigiComp Generic"] + } + } "#; + + + let mut parsed_dp_json: Value = serde_json::from_str(json_data).unwrap(); + + + // Add individual display value + // Set the contentType to a choosen value (currently default to PNG) + parsed_dp_json["individualDisplay"] = Value::Array(vec![image_to_individual_display(image_value)]); + + + parsed_dp_json + + // if let Some(id_value) = identity_value.get("identityHash") { + // if identity_type.eq(&"Student ID".to_string()) { + // let mut new_object = Map::new(); + // new_object.insert("id".to_string(), Value::String("urn:epass:identifier:2".to_string())); + // new_object.insert("type".to_string(), Value::String("Identifier".to_string())); + // new_object.insert("notation".to_string(), id_value.clone()); + // new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); + // let _current_value = Value::Object(new_object); + // _current_value + // } else { + // id_value.clone() + // } + // } else { + // Value::String("".to_string()) + // } +} diff --git a/src/backend/init_conversion.rs b/src/backend/init_conversion.rs index 765f293..9996dea 100644 --- a/src/backend/init_conversion.rs +++ b/src/backend/init_conversion.rs @@ -16,6 +16,7 @@ pub fn init_conversion(state: &mut AppState) { load_mapping_file(state); enter_fixed_context_values(state); enter_fixed_schema_values(state); + enter_credential_profile_values(state); update_display_section(state, false); } @@ -131,6 +132,28 @@ fn enter_fixed_schema_values(state: &mut AppState) { } } +/// Enter fixed values into 'credentialProfile' field, as demanded by the respective json-schema +fn enter_credential_profile_values(state: &mut AppState) { + if state.mapping.output_format() == "ELM" { + let output_elm = state.repository.get_mut("ELM").unwrap().as_object_mut().unwrap(); + output_elm.insert( + "credentialProfiles".to_string(), + Value::Array(vec![ + json!({"id": "http://data.europa.eu/snb/credential/e34929035b","type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/credential/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": {"en": ["Generic"]} + }) + ]), + ); + } else if state.mapping.output_format() == "OBv3" { + let output_obv3 = state.repository.get_mut("OBv3").unwrap().as_object_mut().unwrap(); + } +} + + //////// HELPERS //////// pub fn get_json(path: impl AsRef) -> Result diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 81fbb85..7f3e911 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -3,7 +3,8 @@ use crate::{ jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, transformations::{DataLocation, DataTypeLocation, StringArrayValue, StringValue, Transformation}, - base64_encode::encode_image_from_url, + base64_encode::{image_to_individual_display, create_display_parameter} + }, state::{AppState, Mapping}, trace_dbg, @@ -413,10 +414,12 @@ impl Repository { let mut leaf_node = construct_leaf_node(&pointer); // run the source value through a markdown converter to fit the nested objects into a markdown string - let markdown_source_value = json!(image_to_individual_display(source_value)); +// let image_individualdisplay_source = Value::Array(vec![json!(create_display_parameter(source_value))]); + let image_individualdisplay_source = json!(create_display_parameter(source_value)); +// let markdown_source_value = json!(image_to_individual_display(source_value)); if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(markdown_source_value); + *value = transformation.apply(image_individualdisplay_source); } merge(destination_credential, leaf_node); @@ -532,8 +535,9 @@ fn values_to_identity(identity_type: &str, identity_value: Value) -> Value { new_object.insert("identityType".to_string(), Value::String(identity_type.to_string())); new_object.insert("hashed".to_string(), Value::Bool(false)); new_object.insert("salt".to_string(), Value::String("not-used".to_string())); - let _current_value = Value::Object(new_object); - _current_value +// let _current_value = Value::Object(new_object); + let _array_object = Value::Array(vec![Value::Object(new_object)]); + _array_object } fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { @@ -561,8 +565,8 @@ fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { new_object.insert("type".to_string(), Value::String("Identifier".to_string())); new_object.insert("notation".to_string(), id_value.clone()); new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); - let _current_value = Value::Object(new_object); - _current_value + let _array_object = Value::Array(vec![Value::Object(new_object)]); + _array_object } else { id_value.clone() } @@ -572,105 +576,6 @@ fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { } -fn image_to_individual_display(image_value: Value) -> Value { - //inspect the image object and re write it so it can be reused in ELM - - //we need to achieve the following structure into the indivudualDisplay array: - let json_data = r#" - { - "id": "urn:epass:individualDisplay:c05743e7-9f9d-4e0b-899b-7ae6514c7a02", - "type": "IndividualDisplay", - "language": { - "id": "http://publications.europa.eu/resource/authority/language/ENG", - "type": "Concept", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/language", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": ["English"] - }, - "notation": "language" - }, - "displayDetail": [ - { - "id": "urn:epass:displayDetail:123", - "type": "DisplayDetail", - "image": { - "id": "urn:epass:mediaObject:https://avatars.githubusercontent.com/u/22613412?v=4", - "type": "MediaObject", - "content": "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", - "contentEncoding": { - "id": "http://data.europa.eu/snb/encoding/6146cde7dd", - "type": "Concept", - "inScheme": { - "id": "http://data.europa.eu/snb/encoding/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": ["base64"] - } - }, - "contentType": { - "id": "http://publications.europa.eu/resource/authority/file-type/PNG", - "type": "Concept", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/file-type", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": ["PNG"] - }, - "notation": "file-type" - } - } - } - ] - } - "#; - - - let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); - - // Directly mutate the `content` value - // first try to encode the image in the URL: - let encoded_string = match encode_image_from_url("https://avatars.githubusercontent.com/u/22613412?v=4") { - Ok(encoded_string) => { - println!("Successfully encoded the image."); - encoded_string // Assign the encoded string to the variable - } - Err(e) => { - eprintln!("Error: {}", e); - String::new() // Assign an empty string or a default value in case of an error - } - }; - if let Some(image_content) = parsed_json["displayDetail"][0]["image"]["content"].as_str() { - parsed_json["displayDetail"][0]["image"]["content"] = Value::String(encoded_string); - } else { - println!("Key 'id' in 'language' not found."); - } - - - - println!("{:#?}", parsed_json); - parsed_json - - // if let Some(id_value) = identity_value.get("identityHash") { - // if identity_type.eq(&"Student ID".to_string()) { - // let mut new_object = Map::new(); - // new_object.insert("id".to_string(), Value::String("urn:epass:identifier:2".to_string())); - // new_object.insert("type".to_string(), Value::String("Identifier".to_string())); - // new_object.insert("notation".to_string(), id_value.clone()); - // new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); - // let _current_value = Value::Object(new_object); - // _current_value - // } else { - // id_value.clone() - // } - // } else { - // Value::String("".to_string()) - // } -} fn json_to_markdown(json: &Value, indent_level: usize) -> String { let mut markdown = String::new(); diff --git a/src/backend/routes/root.rs b/src/backend/routes/root.rs index 5d9c3d9..29f6f88 100644 --- a/src/backend/routes/root.rs +++ b/src/backend/routes/root.rs @@ -34,6 +34,9 @@ pub async fn root() -> Html<&'static str> {

+

+ for testing of the created credential in ELM format check here +

"#, diff --git a/src/backend/routes/translate_file.rs b/src/backend/routes/translate_file.rs index 8839498..3836de5 100644 --- a/src/backend/routes/translate_file.rs +++ b/src/backend/routes/translate_file.rs @@ -133,13 +133,16 @@ pub async fn translate_file(mut multipart: Multipart) -> Result Date: Mon, 13 Jan 2025 14:16:51 +0100 Subject: [PATCH 30/45] added EDCI support --- .../custom_mapping_OBv3_ELM_latest.json | 2 +- .../Complete_OpenBadgeCredential_image.json | 2 +- .../theed_regular_embedded_JPG_ho.json | 115 + .../examples/theed_regular_embedded_ho.json | 115 + src/backend/base64_encode.rs | 350 +- src/backend/init_conversion.rs | 7 +- src/backend/mod.rs | 2 +- src/backend/repository.rs | 59 +- src/backend/routes/translate_file.rs | 7 +- src/backend/transformations.rs | 1 - uploads/Bengales_highSchoolDiploma.json | 2925 ----------------- uploads/DigiComp_Generic.json | 612 ---- 12 files changed, 466 insertions(+), 3731 deletions(-) create mode 100644 json/obv3/examples/theed_regular_embedded_JPG_ho.json create mode 100644 json/obv3/examples/theed_regular_embedded_ho.json delete mode 100755 uploads/Bengales_highSchoolDiploma.json delete mode 100755 uploads/DigiComp_Generic.json diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index c19896a..5c3edfe 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -122,7 +122,7 @@ "type_": "imageToIndividualDisplay", "source": { "format": "OBv3", - "path": "$.image" + "path": "$.credentialSubject.achievement.image" }, "destination": { "format": "ELM", diff --git a/json/obv3/examples/Complete_OpenBadgeCredential_image.json b/json/obv3/examples/Complete_OpenBadgeCredential_image.json index 91a285d..59e955c 100644 --- a/json/obv3/examples/Complete_OpenBadgeCredential_image.json +++ b/json/obv3/examples/Complete_OpenBadgeCredential_image.json @@ -314,7 +314,7 @@ "fieldOfStudy": "Research", "humanCode": "R1", "image": { - "id": "https://1edtech.edu/achievements/degree/image", + "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/edubadges_100x100.png", "type": "Image", "caption": "1EdTech University Degree" }, diff --git a/json/obv3/examples/theed_regular_embedded_JPG_ho.json b/json/obv3/examples/theed_regular_embedded_JPG_ho.json new file mode 100644 index 0000000..a4e89fc --- /dev/null +++ b/json/obv3/examples/theed_regular_embedded_JPG_ho.json @@ -0,0 +1,115 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://raw.githubusercontent.com/educredentials/obv3-examples/refs/heads/main/contexts/educredential.json" + ], + "id": "http://example.com/credentials/crd-D4E5F6", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "https://example.com/issuers/iss-9Z8Y7X", + "type": [ + "Profile" + ], + "name": "Naboo Theed University", + "otherIdentifier": [ + { + "type": "IdentifierEntry", + "identifier": "42NB", + "identifierType": "ext:BRIN" + }, + { + "type": "IdentifierEntry", + "identifier": "university.naboo", + "identifierType": "name" + } + ] + }, + "validFrom": "2014-06-01T00:00:00Z", + "name": "The Force and Its Applications", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/ach-77NPN", + "type": [ + "Achievement", + "EducredentialAchievement" + ], + "criteria": { + "narrative": "This badge is awarded for completing the course 'The Force and Its Applications'" + }, + "description": "This badge is awarded for completing the course 'The Force and Its Applications'", + "name": "The Force and Its Applications", + "image": { + "id": "data:image/jpeg;base64,/9j/4Q/+RXhpZgAATU0AKgAAAAgABgESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAABAAIAAAITAAMAAAABAAEAAIdpAAQAAAABAAAAZgAAAAAAAABIAAAAAQAAAEgAAAABAAeQAAAHAAAABDAyMjGRAQAHAAAABAECAwCgAAAHAAAABDAxMDCgAQADAAAAAQABAACgAgAEAAAAAQAAAeCgAwAEAAAAAQAAASykBgADAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA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", + "type": "Image" + }, + "resultDescription": [ + { + "id": "https://example.com/results/ects-nl-NL-A1B2C3", + "type": [ + "ResultDescription" + ], + "valueMax": "10", + "valueMin": "1", + "name": "Final Project Grade", + "requiredValue": "6", + "resultType": "ext:ECTSGradeScore" + } + ], + "inLanguage": "en-EN", + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetType": "ext:QualityAssurance", + "targetName": "M Philosophy of Science, Technology and Society", + "targetDescription": "Accreditatie bestaande opleiding", + "targetCode": "AV-2391", + "targetUrl": "https://data.example.com/decisions/AV-2391" + }, + { + "type": [ + "Alignment" + ], + "targetType": "ext:EQF", + "targetName": "EQF level 5", + "targetCode": "5", + "targetUrl": "https://content.example.com/description-eqf-levels" + } + ], + "educationProgramIdentifier": 133742, + "ECTS": 6.0 + }, + "result": [ + { + "type": [ + "Result" + ], + "resultDescription": "https://example.com/results/ects-nl-NL-D4E5F6", + "value": "8.0" + } + ] + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://raw.githubusercontent.com/educredentials/obv3-examples/refs/heads/main/schemas/regular.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://raw.githubusercontent.com/educredentials/obv3-examples/refs/heads/main/schemas/regular_ects.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ] +} diff --git a/json/obv3/examples/theed_regular_embedded_ho.json b/json/obv3/examples/theed_regular_embedded_ho.json new file mode 100644 index 0000000..01d465a --- /dev/null +++ b/json/obv3/examples/theed_regular_embedded_ho.json @@ -0,0 +1,115 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://raw.githubusercontent.com/educredentials/obv3-examples/refs/heads/main/contexts/educredential.json" + ], + "id": "http://example.com/credentials/crd-D4E5F6", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "https://example.com/issuers/iss-9Z8Y7X", + "type": [ + "Profile" + ], + "name": "Naboo Theed University", + "otherIdentifier": [ + { + "type": "IdentifierEntry", + "identifier": "42NB", + "identifierType": "ext:BRIN" + }, + { + "type": "IdentifierEntry", + "identifier": "university.naboo", + "identifierType": "name" + } + ] + }, + "validFrom": "2014-06-01T00:00:00Z", + "name": "The Force and Its Applications", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "achievement": { + "id": "https://example.com/achievements/ach-77NPN", + "type": [ + "Achievement", + "EducredentialAchievement" + ], + "criteria": { + "narrative": "This badge is awarded for completing the course 'The Force and Its Applications'" + }, + "description": "This badge is awarded for completing the course 'The Force and Its Applications'", + "name": "The Force and Its Applications", + "image": { + "id": "data:image/jpeg;base64,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", + "type": "Image" + }, + "resultDescription": [ + { + "id": "https://example.com/results/ects-nl-NL-A1B2C3", + "type": [ + "ResultDescription" + ], + "valueMax": "10", + "valueMin": "1", + "name": "Final Project Grade", + "requiredValue": "6", + "resultType": "ext:ECTSGradeScore" + } + ], + "inLanguage": "en-EN", + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetType": "ext:QualityAssurance", + "targetName": "M Philosophy of Science, Technology and Society", + "targetDescription": "Accreditatie bestaande opleiding", + "targetCode": "AV-2391", + "targetUrl": "https://data.example.com/decisions/AV-2391" + }, + { + "type": [ + "Alignment" + ], + "targetType": "ext:EQF", + "targetName": "EQF level 5", + "targetCode": "5", + "targetUrl": "https://content.example.com/description-eqf-levels" + } + ], + "educationProgramIdentifier": 133742, + "ECTS": 6.0 + }, + "result": [ + { + "type": [ + "Result" + ], + "resultDescription": "https://example.com/results/ects-nl-NL-D4E5F6", + "value": "8.0" + } + ] + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://raw.githubusercontent.com/educredentials/obv3-examples/refs/heads/main/schemas/regular.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://raw.githubusercontent.com/educredentials/obv3-examples/refs/heads/main/schemas/regular_ects.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ] +} diff --git a/src/backend/base64_encode.rs b/src/backend/base64_encode.rs index b700e92..5331b5b 100644 --- a/src/backend/base64_encode.rs +++ b/src/backend/base64_encode.rs @@ -1,9 +1,10 @@ use base64::{engine::general_purpose::STANDARD as Base64Engine, Engine}; -//use serde::Serialize; +use regex::Regex; +use serde_json::Value; use std::error::Error; use std::io::Read; +use std::path::Path; use ureq::get; -use serde_json::{json, Map, Value}; /// Fetches an image from the given URL, encodes it in Base64, and returns the encoded string. /// @@ -17,14 +18,10 @@ fn encode_image_from_url(url: &str) -> Result> { let resp = get(url).call().expect("Failed to download image"); assert!(resp.has("Content-Length")); - let len: usize = resp.header("Content-Length") - .unwrap() - .parse()?; + let len: usize = resp.header("Content-Length").unwrap().parse()?; let mut bytes: Vec = Vec::with_capacity(len); - resp.into_reader() - .take(10_000_000) - .read_to_end(&mut bytes)?; + resp.into_reader().take(10_000_000).read_to_end(&mut bytes)?; // Encode the image bytes as a Base64 string let base64_string = Base64Engine.encode(&bytes); @@ -33,14 +30,14 @@ fn encode_image_from_url(url: &str) -> Result> { } /// Creates contentType object based on input type in string -/// +/// /// # Arguments /// - `content_type`: The a type that is than rewritten into a serde Value object. /// /// # Returns /// - `Ok(Value)`: The content value Object in ELM format if successful. -/// - `Err(Box)`: An error if the fetch or encoding fails. -fn set_content_type(content_type: &str ) -> Result> { +/// - `Err(Box)`: An error if the fetch or encoding fails. +fn set_content_type(content_type: &str) -> Result> { let content_json = r#" { "id": "http://publications.europa.eu/resource/authority/file-type/PNG", @@ -56,40 +53,45 @@ fn set_content_type(content_type: &str ) -> Result> { } "#; - let mut parsed_contentType_json: Value = serde_json::from_str(content_json).unwrap(); + let mut parsed_content_type_json: Value = serde_json::from_str(content_json).unwrap(); match content_type { "PNG" => { - parsed_contentType_json["id"] = Value::String("http://publications.europa.eu/resource/authority/file-type/PNG".to_string()); - parsed_contentType_json["prefLabel"]["en"][0] = Value::String("PNG".to_string()); + parsed_content_type_json["id"] = + Value::String("http://publications.europa.eu/resource/authority/file-type/PNG".to_string()); + parsed_content_type_json["prefLabel"]["en"][0] = Value::String("PNG".to_string()); } - "JPG" => { - parsed_contentType_json["id"] = Value::String("http://publications.europa.eu/resource/authority/file-type/JPG".to_string()); - parsed_contentType_json["prefLabel"]["en"][0] = Value::String("JPG".to_string()); + "JPG" | "JPEG" => { + parsed_content_type_json["id"] = + Value::String("http://publications.europa.eu/resource/authority/file-type/JPEG".to_string()); + parsed_content_type_json["prefLabel"]["en"][0] = Value::String("JPG".to_string()); } "SVG" => { - parsed_contentType_json["id"] = Value::String("http://publications.europa.eu/resource/authority/file-type/SVG".to_string()); - parsed_contentType_json["prefLabel"]["en"][0] = Value::String("SVG".to_string()); + parsed_content_type_json["id"] = + Value::String("http://publications.europa.eu/resource/authority/file-type/SVG".to_string()); + parsed_content_type_json["prefLabel"]["en"][0] = Value::String("SVG".to_string()); } - _ => { + _ => { // Handle other cases - return Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidInput,"This file Type is not exporteswent wrong!"))); + return Err(Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "This file Type is not exporteswent wrong!", + ))); } } - Ok(parsed_contentType_json) - } - + Ok(parsed_content_type_json) +} /// Creates contentEncodingType object based on input type in string -/// +/// /// # Arguments /// - `content_encoding_type`: The a type that is than rewritten into a serde Value object. /// /// # Returns /// - `Ok(Value)`: The content value Object in ELM format if successful. -/// - `Err(Box)`: An error if the fetch or encoding fails. -fn set_content_enconding_type(content_encoding_type: &str ) -> Result> { - let template_json = r#" +/// - `Err(Box)`: An error if the fetch or encoding fails. +fn set_content_enconding_type(content_encoding_type: &str) -> Result> { + let template_json = r#" { "id": "http://data.europa.eu/snb/encoding/6146cde7dd", "type": "Concept", @@ -103,32 +105,35 @@ fn set_content_enconding_type(content_encoding_type: &str ) -> Result { - parsed_content_encoding_type_json["id"] = Value::String("http://data.europa.eu/snb/encoding/6146cde7dd".to_string()); - parsed_content_encoding_type_json["prefLabel"]["en"][0] = Value::String("base64".to_string()); - } - _ => { - // Handle other cases - return Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidInput,"This content encoding Type is not supported!"))); - } - } - Ok(parsed_content_encoding_type_json) - } + let mut parsed_content_encoding_type_json: Value = serde_json::from_str(template_json).unwrap(); + match content_encoding_type { + "base64" => { + parsed_content_encoding_type_json["id"] = + Value::String("http://data.europa.eu/snb/encoding/6146cde7dd".to_string()); + parsed_content_encoding_type_json["prefLabel"]["en"][0] = Value::String("base64".to_string()); + } + _ => { + // Handle other cases + return Err(Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "This content encoding Type is not supported!", + ))); + } + } + Ok(parsed_content_encoding_type_json) +} /// Creates language object based on input type in string -/// +/// /// # Arguments /// - `content_encoding_type`: The a type that is than rewritten into a serde Value object. /// /// # Returns /// - `Ok(Value)`: The content value Object in ELM format if successful. -/// - `Err(Box)`: An error if the fetch or encoding fails. -fn set_language(language: &str ) -> Result> { - let template_json = r#" +/// - `Err(Box)`: An error if the fetch or encoding fails. +fn set_language(language: &str) -> Result> { + let template_json = r#" { "id": "http://publications.europa.eu/resource/authority/language/ENG", "type": "Concept", @@ -143,29 +148,29 @@ fn set_language(language: &str ) -> Result> { } "#; - let mut parsed_language_json: Value = serde_json::from_str(template_json).unwrap(); - - match language { - "ENG" => { - parsed_language_json["id"] = Value::String("http://publications.europa.eu/resource/authority/language/ENG".to_string()); - parsed_language_json["prefLabel"]["en"][0] = Value::String("English".to_string()); - } - "NLD"=> { - parsed_language_json["id"] = Value::String("http://publications.europa.eu/resource/authority/language/NLD".to_string()); - parsed_language_json["prefLabel"]["en"][0] = Value::String("dutch".to_string()); - } - _ => { - // Handle other cases - return Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidInput,"This language Type is not supported!"))); - } - } - Ok(parsed_language_json) - } - - - - + let mut parsed_language_json: Value = serde_json::from_str(template_json).unwrap(); + match language { + "ENG" => { + parsed_language_json["id"] = + Value::String("http://publications.europa.eu/resource/authority/language/ENG".to_string()); + parsed_language_json["prefLabel"]["en"][0] = Value::String("English".to_string()); + } + "NLD" => { + parsed_language_json["id"] = + Value::String("http://publications.europa.eu/resource/authority/language/NLD".to_string()); + parsed_language_json["prefLabel"]["en"][0] = Value::String("dutch".to_string()); + } + _ => { + // Handle other cases + return Err(Box::new(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "This language Type is not supported!", + ))); + } + } + Ok(parsed_language_json) +} pub fn image_to_individual_display(image_value: Value) -> Value { //inspect the image object and re write it so it can be reused in ELM @@ -207,7 +212,7 @@ pub fn image_to_individual_display(image_value: Value) -> Value { } }, "contentType": { - "id": "http://publications.europa.eu/resource/authority/file-type/JPG", + "id": "http://publications.europa.eu/resource/authority/file-type/JPEG", "type": "Concept", "inScheme": { "id": "http://publications.europa.eu/resource/authority/file-type", @@ -223,33 +228,95 @@ pub fn image_to_individual_display(image_value: Value) -> Value { ] } "#; - let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); - // Directly mutate the `content` value - // first try to encode the image in the URL: - let encoded_string = match encode_image_from_url("https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/edubadges.png") { - Ok(encoded_string) => { - println!("Successfully encoded the image."); - encoded_string // Assign the encoded string to the variable - } - Err(e) => { - eprintln!("Error: {}", e); - String::new() // Assign an empty string or a default value in case of an error + // OB usess the id field to point to an image or have the image encoded. + // Content type is also based on either URL or encoding in the id. + // Extract the `id` field + let mut encoded_string = String::new(); + let mut file_type_sting = String::new(); + if let Some(id_value) = image_value.get("id") { + if let Some(ob3_image_id) = id_value.as_str() { + // Test if `id` is a URL + println!("ob3_image_id value: {}", ob3_image_id); + let url_regex = Regex::new(r"^(https?://[^\s]+)$").unwrap(); + if url_regex.is_match(ob3_image_id) { + println!("The `id` is a valid URL: {}", ob3_image_id); + // Directly mutate the `content` value + // first try to encode the image in the URL: + match encode_image_from_url(ob3_image_id) { + Ok(_encoded_image_string) => { + println!("Successfully encoded the image."); + encoded_string = _encoded_image_string; // Assign the encoded string to the variable + + if let Some(extension) = Path::new(ob3_image_id).extension() { + // Convert the extension to a string + if let Some(ext) = extension.to_str() { + println!("File extension: {}", ext); + file_type_sting = ext.to_ascii_uppercase().to_string(); + } else { + println!("Could not convert extension to string."); + } + } else { + println!("No file extension found."); + } + } + Err(e) => { + eprintln!("Error: {}", e); + encoded_string = String::new(); // Assign an empty string or a default value in case of an error + } + }; + // } else if Base64Engine.decode(ob3_image_id).is_ok() { + } else if ob3_image_id.contains("data") { + // Test if `id` is Base64 encoded + println!("The `id` is a Base64-encoded binary string."); + if let Some((mime_part, content_part)) = ob3_image_id.split_once(',') { + if let Some((_, type_and_enc)) = mime_part.split_once('/') { + if let Some((subtype, _)) = type_and_enc.split_once(';') { + file_type_sting = subtype.to_ascii_uppercase().to_string(); + } + } + encoded_string = content_part.to_string(); + } else { + println!("Invalid data URI format."); + } + } else { + println!("The `id` is neither a URL nor a Base64-encoded string."); + } + } else { + println!("The 'id' field is not a string."); } - }; + } else { + println!("The 'id' field does not exist."); + } + + // // Directly mutate the `content` value + // // first try to encode the image in the URL: + // let encoded_string = match encode_image_from_url(ob3_image_id) { + // Ok(encoded_string) => { + // println!("Successfully encoded the image."); + // encoded_string // Assign the encoded string to the variable + // } + // Err(e) => { + // eprintln!("Error: {}", e); + // String::new() // Assign an empty string or a default value in case of an error + // } + // }; - if let Some(image_content) = parsed_json["displayDetail"][0]["image"]["content"].as_str() { + if let Some(_image_content) = parsed_json["displayDetail"][0]["image"]["content"].as_str() { parsed_json["displayDetail"][0]["image"]["content"] = Value::String(encoded_string); } else { println!("Key 'content' in 'image' not found."); } - // Directly mutate the `contentType` value // Set the contentType to a choosen value (currently default to PNG) - let encoded_content_type = match set_content_type("PNG") { + if file_type_sting.is_empty() { + file_type_sting = "PNG".to_string(); + } + println!("fileTypoe string: {}", file_type_sting); + let encoded_content_type = match set_content_type(file_type_sting.as_str()) { Ok(encoded_content_type) => { println!("Successfully added the contentType."); encoded_content_type // Assign the encoded string to the variable @@ -265,48 +332,44 @@ pub fn image_to_individual_display(image_value: Value) -> Value { } else { println!("Key 'contentType' in 'image' not found."); } - // Directly mutate the `encoding` value // first try to encode the image in the URL: let encoding_value = match set_content_enconding_type("base64") { - Ok(encoding_value) => { - println!("Successfully added encoding type to the image."); - encoding_value // Assign the encoded string to the variable - } - Err(e) => { - eprintln!("Error: {}", e); - Value::Null // Assign an empty string or a default value in case of an error - } - }; - - if let Some(image_encoding) = parsed_json["displayDetail"][0]["image"]["contentEncoding"].as_object() { - parsed_json["displayDetail"][0]["image"]["contentEncoding"] = encoding_value; - } else { - println!("Key 'contentEncoding' in 'image' not found."); - } - + Ok(encoding_value) => { + println!("Successfully added encoding type to the image."); + encoding_value // Assign the encoded string to the variable + } + Err(e) => { + eprintln!("Error: {}", e); + Value::Null // Assign an empty string or a default value in case of an error + } + }; - // Directly mutate the `language` value - // first try to encode the image in the URL: - let language_value = match set_language("ENG") { - Ok(language_value) => { - println!("Successfully added language to the individual display properties."); - language_value // Assign the encoded string to the variable - } - Err(e) => { - eprintln!("Error: {}", e); - Value::Null // Assign an empty string or a default value in case of an error + if let Some(_image_encoding) = parsed_json["displayDetail"][0]["image"]["contentEncoding"].as_object() { + parsed_json["displayDetail"][0]["image"]["contentEncoding"] = encoding_value; + } else { + println!("Key 'contentEncoding' in 'image' not found."); } - }; - - if let Some(_language) = parsed_json["language"].as_object() { - parsed_json["language"] = language_value; - } else { - println!("Key 'language' in 'individualDisplay' not found."); - } + // Directly mutate the `language` value + // first try to encode the image in the URL: + let language_value = match set_language("ENG") { + Ok(language_value) => { + println!("Successfully added language to the individual display properties."); + language_value // Assign the encoded string to the variable + } + Err(e) => { + eprintln!("Error: {}", e); + Value::Null // Assign an empty string or a default value in case of an error + } + }; + if let Some(_language) = parsed_json["language"].as_object() { + parsed_json["language"] = language_value; + } else { + println!("Key 'language' in 'individualDisplay' not found."); + } //println!("{:#?}", parsed_json); parsed_json @@ -328,12 +391,11 @@ pub fn image_to_individual_display(image_value: Value) -> Value { // } } - pub fn create_display_parameter(image_value: Value) -> Value { - //inspect the image object and re write it so it can be reused in ELM + //inspect the image object and re write it so it can be reused in ELM - //we need to achieve the following structure into the indivudualDisplay array: - let json_data = r#" + //we need to achieve the following structure into the indivudualDisplay array: + let json_data = r#" { "id": "urn:epass:displayParameter:1", "type": "DisplayParameter", @@ -374,31 +436,27 @@ pub fn create_display_parameter(image_value: Value) -> Value { } } "#; + let mut parsed_dp_json: Value = serde_json::from_str(json_data).unwrap(); - let mut parsed_dp_json: Value = serde_json::from_str(json_data).unwrap(); - - - // Add individual display value - // Set the contentType to a choosen value (currently default to PNG) - parsed_dp_json["individualDisplay"] = Value::Array(vec![image_to_individual_display(image_value)]); - + // Add individual display value + // Set the contentType to a choosen value (currently default to PNG) + parsed_dp_json["individualDisplay"] = Value::Array(vec![image_to_individual_display(image_value)]); - parsed_dp_json + parsed_dp_json - // if let Some(id_value) = identity_value.get("identityHash") { - // if identity_type.eq(&"Student ID".to_string()) { - // let mut new_object = Map::new(); - // new_object.insert("id".to_string(), Value::String("urn:epass:identifier:2".to_string())); - // new_object.insert("type".to_string(), Value::String("Identifier".to_string())); - // new_object.insert("notation".to_string(), id_value.clone()); - // new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); - // let _current_value = Value::Object(new_object); - // _current_value - // } else { - // id_value.clone() - // } - // } else { - // Value::String("".to_string()) - // } + // if let Some(id_value) = identity_value.get("identityHash") { + // if identity_type.eq(&"Student ID".to_string()) { + // let mut new_object = Map::new(); + // new_object.insert("id".to_string(), Value::String("urn:epass:identifier:2".to_string())); + // new_object.insert("type".to_string(), Value::String("Identifier".to_string())); + // new_object.insert("notation".to_string(), id_value.clone()); + // new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); + // let _current_value = Value::Object(new_object); + // _current_value + // } else { + // id_value.clone() + // } + // } else { + // Value::String("".to_string()) + // } } - diff --git a/src/backend/init_conversion.rs b/src/backend/init_conversion.rs index 9996dea..756acaf 100644 --- a/src/backend/init_conversion.rs +++ b/src/backend/init_conversion.rs @@ -145,15 +145,14 @@ fn enter_credential_profile_values(state: &mut AppState) { "type": "ConceptScheme" }, "prefLabel": {"en": ["Generic"]} - }) - ]), + }), + ]), ); } else if state.mapping.output_format() == "OBv3" { - let output_obv3 = state.repository.get_mut("OBv3").unwrap().as_object_mut().unwrap(); + let _output_obv3 = state.repository.get_mut("OBv3").unwrap().as_object_mut().unwrap(); } } - //////// HELPERS //////// pub fn get_json(path: impl AsRef) -> Result diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 97bf321..56c6e59 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,3 +1,4 @@ +pub mod base64_encode; pub mod candidate_value; pub mod desm_mapping; pub mod getters_resolvers; @@ -11,4 +12,3 @@ pub mod routes; pub mod transformations; pub mod update_display; pub mod web; -pub mod base64_encode; diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 7f3e911..1aa5298 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -1,10 +1,9 @@ use crate::{ backend::{ + base64_encode::create_display_parameter, jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, transformations::{DataLocation, DataTypeLocation, StringArrayValue, StringValue, Transformation}, - base64_encode::{image_to_individual_display, create_display_parameter} - }, state::{AppState, Mapping}, trace_dbg, @@ -414,9 +413,9 @@ impl Repository { let mut leaf_node = construct_leaf_node(&pointer); // run the source value through a markdown converter to fit the nested objects into a markdown string -// let image_individualdisplay_source = Value::Array(vec![json!(create_display_parameter(source_value))]); + // let image_individualdisplay_source = Value::Array(vec![json!(create_display_parameter(source_value))]); let image_individualdisplay_source = json!(create_display_parameter(source_value)); -// let markdown_source_value = json!(image_to_individual_display(source_value)); + // let markdown_source_value = json!(image_to_individual_display(source_value)); if let Some(value) = leaf_node.pointer_mut(&pointer) { *value = transformation.apply(image_individualdisplay_source); @@ -428,8 +427,6 @@ impl Repository { Some((destination_path, source_path)) } - - _ => todo!(), } } @@ -535,9 +532,8 @@ fn values_to_identity(identity_type: &str, identity_value: Value) -> Value { new_object.insert("identityType".to_string(), Value::String(identity_type.to_string())); new_object.insert("hashed".to_string(), Value::Bool(false)); new_object.insert("salt".to_string(), Value::String("not-used".to_string())); -// let _current_value = Value::Object(new_object); - let _array_object = Value::Array(vec![Value::Object(new_object)]); - _array_object + // let _current_value = Value::Object(new_object); + Value::Array(vec![Value::Object(new_object)]) } fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { @@ -565,8 +561,8 @@ fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { new_object.insert("type".to_string(), Value::String("Identifier".to_string())); new_object.insert("notation".to_string(), id_value.clone()); new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); - let _array_object = Value::Array(vec![Value::Object(new_object)]); - _array_object + // return the value arrray + Value::Array(vec![Value::Object(new_object)]) } else { id_value.clone() } @@ -575,8 +571,6 @@ fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { } } - - fn json_to_markdown(json: &Value, indent_level: usize) -> String { let mut markdown = String::new(); let indent = " ".repeat(indent_level); @@ -612,22 +606,20 @@ fn json_to_markdown(json: &Value, indent_level: usize) -> String { } fn markdown_to_json(lines: &[&str]) -> Value { - -// Recursively converts indented lines of Markdown into a JSON structure. -// 1. Parsing Markdown: -// • Headings (#): These are treated as keys in the resulting JSON object. -// • Bold Text (**): This is also treated as a key in the JSON object. -// • List Items (-): These are treated as elements in a JSON array. -// • Plain Text: If it’s not part of a list or a key, it’s treated as a value associated with the last key in the current JSON object. -// 2. Indentation Handling: -// • The code tracks the current indentation level of the Markdown. If the indentation increases, it means a new nested structure (object or array) is starting. If it decreases, the last completed structure is attached to the parent object or array. -// 3. Stack Management: -// • A stack is used to manage the nested structure. Each time a new nested object or array is detected, it’s pushed onto the stack. Once the nesting ends (indentation decreases), the structure is popped from the stack and integrated into the parent structure. -// 4. Regex Patterns: -// • heading_regex: Matches Markdown headings (e.g., # Title). -// • bold_regex: Matches bolded keys (e.g., **Key**:). -// • list_item_regex: Matches list items (e.g., - item). - + // Recursively converts indented lines of Markdown into a JSON structure. + // 1. Parsing Markdown: + // • Headings (#): These are treated as keys in the resulting JSON object. + // • Bold Text (**): This is also treated as a key in the JSON object. + // • List Items (-): These are treated as elements in a JSON array. + // • Plain Text: If it’s not part of a list or a key, it’s treated as a value associated with the last key in the current JSON object. + // 2. Indentation Handling: + // • The code tracks the current indentation level of the Markdown. If the indentation increases, it means a new nested structure (object or array) is starting. If it decreases, the last completed structure is attached to the parent object or array. + // 3. Stack Management: + // • A stack is used to manage the nested structure. Each time a new nested object or array is detected, it’s pushed onto the stack. Once the nesting ends (indentation decreases), the structure is popped from the stack and integrated into the parent structure. + // 4. Regex Patterns: + // • heading_regex: Matches Markdown headings (e.g., # Title). + // • bold_regex: Matches bolded keys (e.g., **Key**:). + // • list_item_regex: Matches list items (e.g., - item). let mut i = 0; let mut position: Vec = Vec::new(); @@ -637,19 +629,15 @@ fn markdown_to_json(lines: &[&str]) -> Value { let mut json_string = String::from(""); // evaluate if the input contains markdown or not // if markdown is detected we will try to create json otherwise the value of the "markdown" will be put straight into the attribute - if lines.contains(&"**") == false { + if !lines.contains(&"**") { while i < lines.len() { let line = lines[i]; json_string.push_str(line); i += 1; } let parsed_json: Value = serde_json::to_value(&json_string).unwrap(); - parsed_json - + parsed_json } else { - - - while i < lines.len() { let line = lines[i]; @@ -849,4 +837,3 @@ fn cleanup_string(string_to_clean: &str) -> String { // Add quotes around the cleaned string format!("\"{}\"", cleaned_string) } - diff --git a/src/backend/routes/translate_file.rs b/src/backend/routes/translate_file.rs index 3836de5..6177e3f 100644 --- a/src/backend/routes/translate_file.rs +++ b/src/backend/routes/translate_file.rs @@ -64,11 +64,11 @@ pub async fn translate_file(mut multipart: Multipart) -> Result { - mapping_file_name = format!("json/mapping/custom_mapping_OBv3_ELM_latest.json"); - mapping_type = Mapping::OBv3ToELM; + mapping_file_name = "json/mapping/custom_mapping_OBv3_ELM_latest.json".to_string(); + mapping_type = Mapping::OBv3ToELM; } "ELMToOBv3" => { - mapping_file_name = format!("json/mapping/custom_mapping_ELM_OBv3_latest.json"); + mapping_file_name = "json/mapping/custom_mapping_ELM_OBv3_latest.json".to_string(); mapping_type = Mapping::ELMToOBv3; } _ => { @@ -103,7 +103,6 @@ pub async fn translate_file(mut multipart: Multipart) -> Result Date: Mon, 13 Jan 2025 20:34:31 +0100 Subject: [PATCH 31/45] remove println! --- src/backend/base64_encode.rs | 50 ++++++++++++++---------------------- 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/src/backend/base64_encode.rs b/src/backend/base64_encode.rs index 5331b5b..acbbea1 100644 --- a/src/backend/base64_encode.rs +++ b/src/backend/base64_encode.rs @@ -239,27 +239,27 @@ pub fn image_to_individual_display(image_value: Value) -> Value { if let Some(id_value) = image_value.get("id") { if let Some(ob3_image_id) = id_value.as_str() { // Test if `id` is a URL - println!("ob3_image_id value: {}", ob3_image_id); + // println!("ob3_image_id value: {}", ob3_image_id); let url_regex = Regex::new(r"^(https?://[^\s]+)$").unwrap(); if url_regex.is_match(ob3_image_id) { - println!("The `id` is a valid URL: {}", ob3_image_id); + // println!("The `id` is a valid URL: {}", ob3_image_id); // Directly mutate the `content` value // first try to encode the image in the URL: match encode_image_from_url(ob3_image_id) { Ok(_encoded_image_string) => { - println!("Successfully encoded the image."); + // println!("Successfully encoded the image."); encoded_string = _encoded_image_string; // Assign the encoded string to the variable if let Some(extension) = Path::new(ob3_image_id).extension() { // Convert the extension to a string if let Some(ext) = extension.to_str() { - println!("File extension: {}", ext); + // println!("File extension: {}", ext); file_type_sting = ext.to_ascii_uppercase().to_string(); } else { - println!("Could not convert extension to string."); + // println!("Could not convert extension to string."); } } else { - println!("No file extension found."); + // println!("No file extension found."); } } Err(e) => { @@ -270,7 +270,7 @@ pub fn image_to_individual_display(image_value: Value) -> Value { // } else if Base64Engine.decode(ob3_image_id).is_ok() { } else if ob3_image_id.contains("data") { // Test if `id` is Base64 encoded - println!("The `id` is a Base64-encoded binary string."); + // println!("The `id` is a Base64-encoded binary string."); if let Some((mime_part, content_part)) = ob3_image_id.split_once(',') { if let Some((_, type_and_enc)) = mime_part.split_once('/') { if let Some((subtype, _)) = type_and_enc.split_once(';') { @@ -279,35 +279,23 @@ pub fn image_to_individual_display(image_value: Value) -> Value { } encoded_string = content_part.to_string(); } else { - println!("Invalid data URI format."); + // println!("Invalid data URI format."); } } else { - println!("The `id` is neither a URL nor a Base64-encoded string."); + // println!("The `id` is neither a URL nor a Base64-encoded string."); } } else { - println!("The 'id' field is not a string."); + // println!("The 'id' field is not a string."); } } else { - println!("The 'id' field does not exist."); + // println!("The 'id' field does not exist."); } - // // Directly mutate the `content` value - // // first try to encode the image in the URL: - // let encoded_string = match encode_image_from_url(ob3_image_id) { - // Ok(encoded_string) => { - // println!("Successfully encoded the image."); - // encoded_string // Assign the encoded string to the variable - // } - // Err(e) => { - // eprintln!("Error: {}", e); - // String::new() // Assign an empty string or a default value in case of an error - // } - // }; if let Some(_image_content) = parsed_json["displayDetail"][0]["image"]["content"].as_str() { parsed_json["displayDetail"][0]["image"]["content"] = Value::String(encoded_string); } else { - println!("Key 'content' in 'image' not found."); + // println!("Key 'content' in 'image' not found."); } // Directly mutate the `contentType` value @@ -315,10 +303,10 @@ pub fn image_to_individual_display(image_value: Value) -> Value { if file_type_sting.is_empty() { file_type_sting = "PNG".to_string(); } - println!("fileTypoe string: {}", file_type_sting); + // println!("fileTypoe string: {}", file_type_sting); let encoded_content_type = match set_content_type(file_type_sting.as_str()) { Ok(encoded_content_type) => { - println!("Successfully added the contentType."); + // println!("Successfully added the contentType."); encoded_content_type // Assign the encoded string to the variable } Err(e) => { @@ -330,14 +318,14 @@ pub fn image_to_individual_display(image_value: Value) -> Value { if let Some(_content_type) = parsed_json["displayDetail"][0]["image"].as_object() { parsed_json["displayDetail"][0]["image"]["contentType"] = encoded_content_type; } else { - println!("Key 'contentType' in 'image' not found."); + // println!("Key 'contentType' in 'image' not found."); } // Directly mutate the `encoding` value // first try to encode the image in the URL: let encoding_value = match set_content_enconding_type("base64") { Ok(encoding_value) => { - println!("Successfully added encoding type to the image."); + // println!("Successfully added encoding type to the image."); encoding_value // Assign the encoded string to the variable } Err(e) => { @@ -349,14 +337,14 @@ pub fn image_to_individual_display(image_value: Value) -> Value { if let Some(_image_encoding) = parsed_json["displayDetail"][0]["image"]["contentEncoding"].as_object() { parsed_json["displayDetail"][0]["image"]["contentEncoding"] = encoding_value; } else { - println!("Key 'contentEncoding' in 'image' not found."); + // println!("Key 'contentEncoding' in 'image' not found."); } // Directly mutate the `language` value // first try to encode the image in the URL: let language_value = match set_language("ENG") { Ok(language_value) => { - println!("Successfully added language to the individual display properties."); + // println!("Successfully added language to the individual display properties."); language_value // Assign the encoded string to the variable } Err(e) => { @@ -368,7 +356,7 @@ pub fn image_to_individual_display(image_value: Value) -> Value { if let Some(_language) = parsed_json["language"].as_object() { parsed_json["language"] = language_value; } else { - println!("Key 'language' in 'individualDisplay' not found."); + // println!("Key 'language' in 'individualDisplay' not found."); } //println!("{:#?}", parsed_json); From 264255d6e9a5db3e1be958b7675019f6b7883eb3 Mon Sep 17 00:00:00 2001 From: hamrt Date: Mon, 20 Jan 2025 17:48:49 +0100 Subject: [PATCH 32/45] clean-up and add example --- README.md | 2 +- .../custom_mapping_example_OB2ELM.json | 275 +++++++ src/backend/base64_encode.rs | 16 +- test/OBv3_example.json | 723 ++++++++++++++++++ 4 files changed, 1000 insertions(+), 16 deletions(-) create mode 100644 json/mapping/custom_mapping_example_OB2ELM.json create mode 100644 test/OBv3_example.json diff --git a/README.md b/README.md index 9fb406f..7affe2d 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ cargo run -- -h example: ```sh -cargo run -- -i example.json -o example_123.json -m example_mapping.json -c OBv3toELM +cargo run -- -i ./test/OBv3_example.json -o ./test/ELM_export_example.json -m ./json/mapping/custom_mapping_example_OB2ELM.json -c OBv3toELM ``` Or find the executable in the `/target/debug` folder named after the repo name `credential-converter`. diff --git a/json/mapping/custom_mapping_example_OB2ELM.json b/json/mapping/custom_mapping_example_OB2ELM.json new file mode 100644 index 0000000..5c3edfe --- /dev/null +++ b/json/mapping/custom_mapping_example_OB2ELM.json @@ -0,0 +1,275 @@ +[ + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.@context" + }, + "destination": { + "format": "ELM", + "path": "$.@context" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.id" + }, + "destination": { + "format": "ELM", + "path": "$.id" + } + }, + { + "type_": "stringArrayIt", + "source": { + "value": ["VerifiableCredential", "VerifiableAttestation", "EuropeanDigitalCredential"] + }, + "destination": { + "format": "ELM", + "path": "$.type" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.issuer.id" + }, + "destination": { + "format": "ELM", + "path": "$.issuer.id" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.issuer.name" + }, + "destination": { + "format": "ELM", + "path": "$.issuer.legalName.en" + } + }, + { + "type_": "stringit", + "source": { + "value": "Organisation" + }, + "destination": { + "format": "ELM", + "path": "$.issuer.type" + } + }, + + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.validFrom" + }, + "destination": { + "format": "ELM", + "path": "$.validFrom" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.validFrom" + }, + "destination": { + "format": "ELM", + "path": "$.issuanceDate" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.validFrom" + }, + "destination": { + "format": "ELM", + "path": "$.issued" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.id" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.id" + } + }, + { + "type_": "stringit", + "source": { + "value": "Person" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.type" + } + }, + { + "type_": "imageToIndividualDisplay", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.image" + }, + "destination": { + "format": "ELM", + "path": "$.displayParameter" + } + }, + { + "type_": "stringit", + "source": { + "value": "LearningAchievement" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].type" + } + }, + { + "type_": "stringit", + "source": { + "value": "urn:epass:awardingProcess:1" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].awardedBy.id" + } + }, + { + "type_": "stringit", + "source": { + "value": "AwardingProcess" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].awardedBy.type" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.issuer.id" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].awardedBy.awardingBody[0].id" + } + }, + { + "type_": "stringit", + "source": { + "value": "Organisation" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].awardedBy.awardingBody[0].type" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.issuer.name" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].awardedBy.awardingBody[0].legalName.en[0]" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.name" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].title.en[0]" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.id" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].id" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSchema" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSchema" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "Student ID", + "path": "$.credentialSubject.identifier[0]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.identifier" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "ext:givenName", + "path": "$.credentialSubject.identifier[1]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.fullName.en[0]" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "ext:familyName", + "path": "$.credentialSubject.identifier[2]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.fullName" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "ext:fullName", + "path": "$.credentialSubject.identifier[3]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.fullName" + } + } +] \ No newline at end of file diff --git a/src/backend/base64_encode.rs b/src/backend/base64_encode.rs index acbbea1..acc8bc9 100644 --- a/src/backend/base64_encode.rs +++ b/src/backend/base64_encode.rs @@ -362,21 +362,7 @@ pub fn image_to_individual_display(image_value: Value) -> Value { //println!("{:#?}", parsed_json); parsed_json - // if let Some(id_value) = identity_value.get("identityHash") { - // if identity_type.eq(&"Student ID".to_string()) { - // let mut new_object = Map::new(); - // new_object.insert("id".to_string(), Value::String("urn:epass:identifier:2".to_string())); - // new_object.insert("type".to_string(), Value::String("Identifier".to_string())); - // new_object.insert("notation".to_string(), id_value.clone()); - // new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); - // let _current_value = Value::Object(new_object); - // _current_value - // } else { - // id_value.clone() - // } - // } else { - // Value::String("".to_string()) - // } + } pub fn create_display_parameter(image_value: Value) -> Value { diff --git a/test/OBv3_example.json b/test/OBv3_example.json new file mode 100644 index 0000000..59e955c --- /dev/null +++ b/test/OBv3_example.json @@ -0,0 +1,723 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://purl.imsglobal.org/spec/ob/v3p0/extensions.json" + ], + "id": "http://1edtech.edu/credentials/3732", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "name": "1EdTech University Degree for Example Student", + "description": "1EdTech University Degree Description", + "image": { + "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/edubadges_100x100.png", + "type": "Image", + "caption": "1EdTech University Degree for Example Student" + }, + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "type": [ + "AchievementSubject" + ], + "activityEndDate": "2010-01-02T00:00:00Z", + "activityStartDate": "2010-01-01T00:00:00Z", + "creditsEarned": 42, + "licenseNumber": "A-9320041", + "role": "Major Domo", + "source": { + "id": "https://school.edu/issuers/201234", + "type": [ + "Profile" + ], + "name": "1EdTech College of Arts" + }, + "term": "Fall", + "identifier": [ + { + "type": "IdentityObject", + "identityHash": "student@1edtech.edu", + "identityType": "emailAddress", + "hashed": false, + "salt": "not-used" + }, + { + "type": "IdentityObject", + "identityHash": "somebody@gmail.com", + "identityType": "emailAddress", + "hashed": false, + "salt": "not-used" + } + ], + "achievement": { + "id": "https://1edtech.edu/achievements/degree", + "type": [ + "Achievement" + ], + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "degree", + "targetDescription": "1EdTech University Degree programs.", + "targetName": "1EdTech University Degree", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFItem", + "targetUrl": "https://1edtech.edu/catalog/degree" + }, + { + "type": [ + "Alignment" + ], + "targetCode": "degree", + "targetDescription": "1EdTech University Degree programs.", + "targetName": "1EdTech University Degree", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CTDL", + "targetUrl": "https://credentialengineregistry.org/resources/ce-98cb027b-95ef-4494-908d-6f7790ec6b6b" + } + ], + "achievementType": "Degree", + "creator": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "1EdTech University", + "url": "https://1edtech.edu", + "phone": "1-222-333-4444", + "description": "1EdTech University provides online degree programs.", + "endorsement": [ + { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "id": "http://1edtech.edu/endorsementcredential/3732", + "type": [ + "VerifiableCredential", + "EndorsementCredential" + ], + "name": "SDE endorsement", + "issuer": { + "id": "https://accrediter.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example Accrediting Agency" + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2020-01-01T00:00:00Z", + "credentialSubject": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "EndorsementSubject" + ], + "endorsementComment": "1EdTech University is in good standing" + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://accrediter.edu/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialStatus": { + "id": "https://1edtech.edu/credentials/3732/revocations", + "type": "1EdTechRevocationList" + }, + "refreshService": { + "id": "http://1edtech.edu/credentials/3732", + "type": "1EdTechCredentialRefresh" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-rdf-2022", + "created": "2022-05-26T18:17:08Z", + "verificationMethod": "https://accrediter.edu/issuers/565049#zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA", + "proofPurpose": "assertionMethod", + "proofValue": "zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA" + } + ] + }, + { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "id": "http://1edtech.edu/endorsementcredential/3733", + "type": [ + "VerifiableCredential", + "EndorsementCredential" + ], + "name": "SDE endorsement", + "issuer": { + "id": "https://state.gov/issuers/565049", + "type": [ + "Profile" + ], + "name": "State Department of Education" + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2020-01-01T00:00:00Z", + "credentialSubject": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "EndorsementSubject" + ], + "endorsementComment": "1EdTech University is in good standing" + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://state.gov/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialStatus": { + "id": "https://state.gov/credentials/3732/revocations", + "type": "1EdTechRevocationList" + }, + "refreshService": { + "id": "http://state.gov/credentials/3732", + "type": "1EdTechCredentialRefresh" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-rdf-2022", + "created": "2022-05-26T18:25:59Z", + "verificationMethod": "https://accrediter.edu/issuers/565049#z5bDnmSgDczXwZGya6ZjxKaxkdKxzsCMiVSsgEVWxnaWK7ZqbKnzcCd7mUKE9DQaAL2QMXP5AquPeW6W2CWrZ7jNC", + "proofPurpose": "assertionMethod", + "proofValue": "z5bDnmSgDczXwZGya6ZjxKaxkdKxzsCMiVSsgEVWxnaWK7ZqbKnzcCd7mUKE9DQaAL2QMXP5AquPeW6W2CWrZ7jNC" + } + ] + } + ], + "image": { + "id": "https://1edtech.edu/logo.png", + "type": "Image", + "caption": "1EdTech University logo" + }, + "email": "registrar@1edtech.edu", + "address": { + "type": [ + "Address" + ], + "addressCountry": "USA", + "addressCountryCode": "US", + "addressRegion": "TX", + "addressLocality": "Austin", + "streetAddress": "123 First St", + "postOfficeBoxNumber": "1", + "postalCode": "12345", + "geo": { + "type": "GeoCoordinates", + "latitude": 1, + "longitude": 1 + } + }, + "otherIdentifier": [ + { + "type": "IdentifierEntry", + "identifier": "12345", + "identifierType": "sourcedId" + }, + { + "type": "IdentifierEntry", + "identifier": "67890", + "identifierType": "nationalIdentityNumber" + } + ], + "official": "Horace Mann", + "parentOrg": { + "id": "did:example:123456789", + "type": [ + "Profile" + ], + "name": "Universal Universities" + } + }, + "creditsAvailable": 36, + "criteria": { + "id": "https://1edtech.edu/achievements/degree", + "narrative": "# Degree Requirements\nStudents must complete..." + }, + "description": "1EdTech University Degree Description", + "endorsement": [ + { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "id": "http://1edtech.edu/endorsementcredential/3734", + "type": [ + "VerifiableCredential", + "EndorsementCredential" + ], + "name": "EAA endorsement", + "issuer": { + "id": "https://accrediter.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example Accrediting Agency" + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2020-01-01T00:00:00Z", + "credentialSubject": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "EndorsementSubject" + ], + "endorsementComment": "1EdTech University is in good standing" + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://accrediter.edu/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialStatus": { + "id": "https://1edtech.edu/credentials/3732/revocations", + "type": "1EdTechRevocationList" + }, + "refreshService": { + "id": "http://1edtech.edu/credentials/3732", + "type": "1EdTechCredentialRefresh" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-rdf-2022", + "created": "2022-05-26T18:17:08Z", + "verificationMethod": "https://accrediter.edu/issuers/565049#zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA", + "proofPurpose": "assertionMethod", + "proofValue": "zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA" + } + ] + } + ], + "fieldOfStudy": "Research", + "humanCode": "R1", + "image": { + "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/edubadges_100x100.png", + "type": "Image", + "caption": "1EdTech University Degree" + }, + "name": "1EdTech University Degree", + "otherIdentifier": [ + { + "type": "IdentifierEntry", + "identifier": "abde", + "identifierType": "identifier" + } + ], + "resultDescription": [ + { + "id": "urn:uuid:f6ab24cd-86e8-4eaf-b8c6-ded74e8fd41c", + "type": [ + "ResultDescription" + ], + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "project", + "targetDescription": "Project description", + "targetName": "Final Project", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFItem", + "targetUrl": "https://1edtech.edu/catalog/degree/project" + } + ], + "allowedValue": [ + "D", + "C", + "B", + "A" + ], + "name": "Final Project Grade", + "requiredValue": "C", + "resultType": "LetterGrade" + }, + { + "id": "urn:uuid:a70ddc6a-4c4a-4bd8-8277-cb97c79f40c5", + "type": [ + "ResultDescription" + ], + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "project", + "targetDescription": "Project description", + "targetName": "Final Project", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFItem", + "targetUrl": "https://1edtech.edu/catalog/degree/project" + } + ], + "allowedValue": [ + "D", + "C", + "B", + "A" + ], + "name": "Final Project Grade", + "requiredLevel": "urn:uuid:d05a0867-d0ad-4b03-bdb5-28fb5d2aab7a", + "resultType": "RubricCriterionLevel", + "rubricCriterionLevel": [ + { + "id": "urn:uuid:d05a0867-d0ad-4b03-bdb5-28fb5d2aab7a", + "type": [ + "RubricCriterionLevel" + ], + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "project", + "targetDescription": "Project description", + "targetName": "Final Project", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFRubricCriterionLevel", + "targetUrl": "https://1edtech.edu/catalog/degree/project/rubric/levels/mastered" + } + ], + "description": "The author demonstrated...", + "level": "Mastered", + "name": "Mastery", + "points": "4" + }, + { + "id": "urn:uuid:6b84b429-31ee-4dac-9d20-e5c55881f80e", + "type": [ + "RubricCriterionLevel" + ], + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "project", + "targetDescription": "Project description", + "targetName": "Final Project", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFRubricCriterionLevel", + "targetUrl": "https://1edtech.edu/catalog/degree/project/rubric/levels/basic" + } + ], + "description": "The author demonstrated...", + "level": "Basic", + "name": "Basic", + "points": "4" + } + ] + }, + { + "id": "urn:uuid:b07c0387-f2d6-4b65-a3f4-f4e4302ea8f7", + "type": [ + "ResultDescription" + ], + "name": "Project Status", + "resultType": "Status" + } + ], + "specialization": "Computer Science Research", + "tag": [ + "research", + "computer science" + ] + }, + "image": { + "id": "https://1edtech.edu/credentials/3732/image", + "type": "Image", + "caption": "1EdTech University Degree for Example Student" + }, + "narrative": "There is a final project report and source code evidence.", + "result": [ + { + "type": [ + "Result" + ], + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "project", + "targetDescription": "Project description", + "targetName": "Final Project", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFItem", + "targetUrl": "https://1edtech.edu/catalog/degree/project/result/1" + } + ], + "resultDescription": "urn:uuid:f6ab24cd-86e8-4eaf-b8c6-ded74e8fd41c", + "value": "A" + }, + { + "type": [ + "Result" + ], + "achievedLevel": "urn:uuid:d05a0867-d0ad-4b03-bdb5-28fb5d2aab7a", + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetCode": "project", + "targetDescription": "Project description", + "targetName": "Final Project", + "targetFramework": "1EdTech University Program and Course Catalog", + "targetType": "CFItem", + "targetUrl": "https://1edtech.edu/catalog/degree/project/result/1" + } + ], + "resultDescription": "urn:uuid:f6ab24cd-86e8-4eaf-b8c6-ded74e8fd41c" + }, + { + "type": [ + "Result" + ], + "resultDescription": "urn:uuid:f6ab24cd-86e8-4eaf-b8c6-ded74e8fd41c", + "status": "Completed" + } + ] + }, + "endorsement": [ + { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "id": "http://1edtech.edu/endorsementcredential/3735", + "type": [ + "VerifiableCredential", + "EndorsementCredential" + ], + "name": "EAA endorsement", + "issuer": { + "id": "https://accrediter.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example Accrediting Agency" + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2020-01-01T00:00:00Z", + "credentialSubject": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "EndorsementSubject" + ], + "endorsementComment": "1EdTech University is in good standing" + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://accrediter.edu/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialStatus": { + "id": "https://1edtech.edu/credentials/3732/revocations", + "type": "1EdTechRevocationList" + }, + "refreshService": { + "id": "http://1edtech.edu/credentials/3732", + "type": "1EdTechCredentialRefresh" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-rdf-2022", + "created": "2022-05-26T18:17:08Z", + "verificationMethod": "https://accrediter.edu/issuers/565049#zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA", + "proofPurpose": "assertionMethod", + "proofValue": "zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA" + } + ] + } + ], + "evidence": [ + { + "id": "https://1edtech.edu/credentials/3732/evidence/1", + "type": [ + "Evidence" + ], + "narrative": "# Final Project Report \n This project was ...", + "name": "Final Project Report", + "description": "This is the final project report.", + "genre": "Research", + "audience": "Department" + }, + { + "id": "https://github.com/somebody/project", + "type": [ + "Evidence" + ], + "name": "Final Project Code", + "description": "This is the source code for the final project app.", + "genre": "Research", + "audience": "Department" + } + ], + "issuer": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "1EdTech University", + "url": "https://1edtech.edu", + "phone": "1-222-333-4444", + "description": "1EdTech University provides online degree programs.", + "endorsement": [ + { + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json" + ], + "id": "http://1edtech.edu/endorsementcredential/3736", + "type": [ + "VerifiableCredential", + "EndorsementCredential" + ], + "name": "EAA endorsement", + "issuer": { + "id": "https://accrediter.edu/issuers/565049", + "type": [ + "Profile" + ], + "name": "Example Accrediting Agency" + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2020-01-01T00:00:00Z", + "credentialSubject": { + "id": "https://1edtech.edu/issuers/565049", + "type": [ + "EndorsementSubject" + ], + "endorsementComment": "1EdTech University is in good standing" + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_endorsementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://accrediter.edu/schema/endorsementcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialStatus": { + "id": "https://1edtech.edu/credentials/3732/revocations", + "type": "1EdTechRevocationList" + }, + "refreshService": { + "id": "http://1edtech.edu/credentials/3732", + "type": "1EdTechCredentialRefresh" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-rdf-2022", + "created": "2022-05-26T18:17:08Z", + "verificationMethod": "https://accrediter.edu/issuers/565049#zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA", + "proofPurpose": "assertionMethod", + "proofValue": "zvPkQiUFfJrgnCRhyPkTSkgrGXbnLR15pHH5HZVYNdM4TCAwQHqG7fMeMPLtYNRnEgoV1aJdR5E61eWu5sWRYgtA" + } + ] + } + ], + "image": { + "id": "https://1edtech.edu/logo.png", + "type": "Image", + "caption": "1EdTech University logo" + }, + "email": "registrar@1edtech.edu", + "address": { + "type": [ + "Address" + ], + "addressCountry": "USA", + "addressCountryCode": "US", + "addressRegion": "TX", + "addressLocality": "Austin", + "streetAddress": "123 First St", + "postOfficeBoxNumber": "1", + "postalCode": "12345", + "geo": { + "type": "GeoCoordinates", + "latitude": 1, + "longitude": 1 + } + }, + "otherIdentifier": [ + { + "type": "IdentifierEntry", + "identifier": "12345", + "identifierType": "sourcedId" + }, + { + "type": "IdentifierEntry", + "identifier": "67890", + "identifierType": "nationalIdentityNumber" + } + ], + "official": "Horace Mann", + "parentOrg": { + "id": "did:example:123456789", + "type": [ + "Profile" + ], + "name": "Universal Universities" + } + }, + "validFrom": "2010-01-01T00:00:00Z", + "validUntil": "2030-01-01T00:00:00Z", + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ], + "credentialStatus": { + "id": "https://1edtech.edu/credentials/3732/revocations", + "type": "1EdTechRevocationList" + }, + "refreshService": { + "id": "http://1edtech.edu/credentials/3732", + "type": "1EdTechCredentialRefresh" + }, + "proof": [ + { + "type": "DataIntegrityProof", + "created": "2024-05-31T14:05:25Z", + "verificationMethod": "https://1edtech.edu/issuers/565049#z6MkphU6QmojC6GdUBNYypgnGaiL2TLisLMxpE1oZcmKg7Ad", + "cryptosuite": "eddsa-rdfc-2022", + "proofPurpose": "assertionMethod", + "proofValue": "z5A4ZXLJa4dUArTmpdP9vnrYijMLCT1tR9KWaFmLT2PeQp3gSnGA9wrRJqrJ5Z8YnpVDxZQWRGjjWNbj2PKDJe7dt" + } + ] +} From b2eb85176c9d4654d0d9eea6c74497c9ed7b6fdb Mon Sep 17 00:00:00 2001 From: hamrt Date: Tue, 28 Jan 2025 17:23:21 +0100 Subject: [PATCH 33/45] fix for location and page support in ELM --- Cargo.lock | 276 +++++++++++++++++- Cargo.toml | 1 + README.md | 10 +- .../custom_mapping_OBv3_ELM_latest.json | 12 +- .../custom_mapping_OBv3_ELM_latest_1.json | 224 -------------- src/backend/base64_encode.rs | 6 +- src/backend/elm_mapping_helper.rs | 70 +++++ src/backend/mod.rs | 1 + src/backend/repository.rs | 49 ++++ src/backend/transformations.rs | 19 ++ test/ELM_export_example.json | 213 ++++++++++++++ test/Quick_test.md | 4 + 12 files changed, 652 insertions(+), 233 deletions(-) delete mode 100644 json/mapping/custom_mapping_OBv3_ELM_latest_1.json create mode 100644 src/backend/elm_mapping_helper.rs create mode 100644 test/ELM_export_example.json create mode 100644 test/Quick_test.md diff --git a/Cargo.lock b/Cargo.lock index 682fab5..6de34f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,6 +307,12 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.7.2" @@ -355,6 +361,28 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "clap" version = "4.5.7" @@ -395,6 +423,42 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +[[package]] +name = "codes-agency" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809a7790247f949cc35a1f9b497fd13a79ee330e34f049c6e837685363d20faf" +dependencies = [ + "codes-common", + "serde", +] + +[[package]] +name = "codes-common" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b16b1037e7111ea4a4bd4a2b48733b990a76ecc321539858a5ab534792b287" +dependencies = [ + "csv", + "tera", + "tracing", +] + +[[package]] +name = "codes-iso-3166" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d9f6ddc6481f15051ec704400c275b5135896006edd9342db7d0a116c70fb4a" +dependencies = [ + "codes-agency", + "codes-common", + "csv", + "lazy_static", + "regex", + "serde", + "tera", +] + [[package]] name = "color-eyre" version = "0.6.3" @@ -532,6 +596,7 @@ dependencies = [ "axum", "base64 0.21.7", "clap", + "codes-iso-3166", "color-eyre", "config", "crossterm", @@ -656,6 +721,12 @@ dependencies = [ "serde", ] +[[package]] +name = "deunicode" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" + [[package]] name = "digest" version = "0.10.7" @@ -908,6 +979,17 @@ dependencies = [ "walkdir", ] +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.6.0", + "ignore", + "walkdir", +] + [[package]] name = "h2" version = "0.3.26" @@ -1029,6 +1111,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "hyper" version = "0.14.29" @@ -1285,6 +1376,12 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + [[package]] name = "libredox" version = "0.1.3" @@ -1627,6 +1724,15 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + [[package]] name = "paste" version = "1.0.15" @@ -1690,6 +1796,44 @@ dependencies = [ "sha2", ] +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -1708,6 +1852,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -1735,6 +1888,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "ratatui" version = "0.26.3" @@ -1909,7 +2092,7 @@ version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dcd94370631e5658a0a23635f7f47e43d06a00ad948e0bb5de79b00d85b880c" dependencies = [ - "globwalk", + "globwalk 0.8.1", "once_cell", "regex", "rust-i18n-macro", @@ -1941,7 +2124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "399801f4d955abf1c3ce3ce2215dc76bd40beb4ae39e3a84936b21a79ce2caa5" dependencies = [ "arc-swap", - "globwalk", + "globwalk 0.8.1", "lazy_static", "normpath", "once_cell", @@ -2163,6 +2346,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -2172,6 +2361,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -2307,6 +2506,28 @@ dependencies = [ "libc", ] +[[package]] +name = "tera" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk 0.9.1", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + [[package]] name = "thiserror" version = "1.0.63" @@ -2682,6 +2903,56 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -3117,6 +3388,7 @@ version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ + "byteorder", "zerocopy-derive", ] diff --git a/Cargo.toml b/Cargo.toml index 9518151..0bc3755 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +codes-iso-3166 = {version="0.1.5", features =["alpha_3_code", "full_name"]} clap = { version = "4", features = ["derive"] } jsonschema = "0.17" tokio = { version = "1", features = ["full"] } diff --git a/README.md b/README.md index 7affe2d..f5e7fee 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,9 @@ Or find the executable in the `/target/debug` folder named after the repo name ` ./target/debug/credential-converter -i example.json -o example_123.json -m example_mapping.json -c OBv3toELM ``` +*Warning: the ratatui library does not seem to handle different color settings in your terminal perfectly. This causes the colors to differ slightly between builds in different terminals. For reference please continue reading the readme, colors will be explained accompanied by screenshots.* + +## Setup webservice For headless/webservice execution: Run with -w @@ -63,10 +66,13 @@ example for running on 192.168.1.1:5000 cargo run -- -w 192.168.1.1:5000 ``` -a webpage displaying a form can be found at the root of the project a webservice api can be found at /translate_file +## Usage of webservice (client-side) +A webpage displaying a form can be found at the root of the project a webservice api can be found at /translate_file +you could use the website to translate the files by surfing to 127.0.0.1:3000/translate_file and providing information in the form presented. +An other option is using the API directly: +```curl 127.0.0.1:3000/translate_file -F translation=OBv3ToELM -F input_file=@test/OBv3_example.json``` -*Warning: the ratatui library does not seem to handle different color settings in your terminal perfectly. This causes the colors to differ slightly between builds in different terminals. For reference please continue reading the readme, colors will be explained accompanied by screenshots.* ## Usage diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index 5c3edfe..c1b3681 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -63,7 +63,17 @@ "path": "$.issuer.type" } }, - + { + "type_": "addressToLocation", + "source": { + "format": "OBv3", + "path": "$.issuer.address" + }, + "destination": { + "format": "ELM", + "path": "$.issuer.location" + } + }, { "type_": "copy", "source": { diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest_1.json b/json/mapping/custom_mapping_OBv3_ELM_latest_1.json deleted file mode 100644 index 55eaa6b..0000000 --- a/json/mapping/custom_mapping_OBv3_ELM_latest_1.json +++ /dev/null @@ -1,224 +0,0 @@ -[ - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.@context" - }, - "destination": { - "format": "ELM", - "path": "$.@context" - } - }, - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.id" - }, - "destination": { - "format": "ELM", - "path": "$.id" - } - }, - { - "type_": "stringArrayIt", - "source": { - "value": ["VerifiableCredential", "VerifiableAttestation", "EuropeanDigitalCredential"] - }, - "destination": { - "format": "ELM", - "path": "$.type" - } - }, - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.issuer.id" - }, - "destination": { - "format": "ELM", - "path": "$.issuer.id" - } - }, - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.issuer.name" - }, - "destination": { - "format": "ELM", - "path": "$.issuer.legalName.en" - } - }, - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.validFrom" - }, - "destination": { - "format": "ELM", - "path": "$.validFrom" - } - }, - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.validFrom" - }, - "destination": { - "format": "ELM", - "path": "$.issuanceDate" - } - }, - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.validFrom" - }, - "destination": { - "format": "ELM", - "path": "$.issued" - } - }, - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.credentialSubject.id" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSubject.id" - } - }, - { - "type_": "stringit", - "source": { - "value": "Person" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSubject.type" - } - }, - { - "type_": "markdownToJson", - "source": { - "format": "OBv3", - "path": "$.credentialSubject.achievement.criteria.narrative" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome[0]" - } - }, - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.name" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].title.en[0]" - } - }, - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.credentialSubject.achievement.id" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].id" - } - }, - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.credentialSubject.achievement.name" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].specifiedBy.title.en[0]" - } - }, - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.credentialSchema" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSchema" - } - }, - { - "type_": "copy", - "source": { - "format": "OBv3", - "path": "$.credentialStatus" - }, - "destination": { - "format": "ELM", - "path": "$.credentialStatus" - } - }, - { - "type_": "identifierToObject", - "source": { - "format": "OBv3", - "datatype": "Student ID", - "path": "$.credentialSubject.identifier[0]" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSubject.identifier" - } - }, - { - "type_": "identifierToObject", - "source": { - "format": "OBv3", - "datatype": "ext:givenName", - "path": "$.credentialSubject.identifier[1]" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSubject.givenName.en[0]" - } - }, - { - "type_": "identifierToObject", - "source": { - "format": "OBv3", - "datatype": "ext:familyName", - "path": "$.credentialSubject.identifier[2]" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSubject.givenName" - } - }, - { - "type_": "identifierToObject", - "source": { - "format": "OBv3", - "datatype": "ext:fullName", - "path": "$.credentialSubject.identifier[3]" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSubject.fullName" - } - } -] \ No newline at end of file diff --git a/src/backend/base64_encode.rs b/src/backend/base64_encode.rs index acc8bc9..47d0fee 100644 --- a/src/backend/base64_encode.rs +++ b/src/backend/base64_encode.rs @@ -211,6 +211,7 @@ pub fn image_to_individual_display(image_value: Value) -> Value { "en": ["base64"] } }, + "page": 1, "contentType": { "id": "http://publications.europa.eu/resource/authority/file-type/JPEG", "type": "Concept", @@ -259,7 +260,7 @@ pub fn image_to_individual_display(image_value: Value) -> Value { // println!("Could not convert extension to string."); } } else { - // println!("No file extension found."); + // println!("No file extension found."); } } Err(e) => { @@ -291,7 +292,6 @@ pub fn image_to_individual_display(image_value: Value) -> Value { // println!("The 'id' field does not exist."); } - if let Some(_image_content) = parsed_json["displayDetail"][0]["image"]["content"].as_str() { parsed_json["displayDetail"][0]["image"]["content"] = Value::String(encoded_string); } else { @@ -361,8 +361,6 @@ pub fn image_to_individual_display(image_value: Value) -> Value { //println!("{:#?}", parsed_json); parsed_json - - } pub fn create_display_parameter(image_value: Value) -> Value { diff --git a/src/backend/elm_mapping_helper.rs b/src/backend/elm_mapping_helper.rs new file mode 100644 index 0000000..9d9aebe --- /dev/null +++ b/src/backend/elm_mapping_helper.rs @@ -0,0 +1,70 @@ +use codes_iso_3166::part_1::CountryCode; +use std::str::FromStr; +use serde_json::Value; + + +/// Creates country code based on input type in string found in addressCountryCode +/// +/// # Arguments +/// - `country_code`: the code found in . +/// +/// # Returns +/// - Value: The content value Object in ELM format if successful. +pub fn address_to_location(address_value: Value) -> Value { + + //inspect the address object (address as used in issuer for now) and re write it so it can be reused in ELM + //we need to achieve the following structure into the indivudualDisplay array: + let json_data = r#" + { + "id": "urn:epass:certificateLocation:1", + "type": "Location", + "address": { + "id": "urn:epass:certificateAddress:1", + "type": "Address", + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/country/ESP", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { "en": "Spain" } + } + } + } + "#; + + let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); + + // set country code default to NL + let mut country = CountryCode::NL; + + // Directly mutate the `Location` value + // Access the "addressCountryCode" field + if let Some(country_code) = address_value.get("addressCountryCode") { + if let Some(country_code_str) = country_code.as_str() { + if country_code_str.is_empty() { + //println!("The addressCountryCode is empty."); + country = CountryCode::from_str("NLD").unwrap(); + } else { + country = CountryCode::from_str(country_code_str).unwrap(); + //println!("The addressCountryCode is: {}", country_code_str); + } + } else { + //println!("The addressCountryCode is not a string."); + } + } else { + //println!("The addressCountryCode field does not exist."); + } + + parsed_json["address"]["countryCode"]["id"] = Value::String(format!("http://publications.europa.eu/resource/authority/language/{}",country.alpha_3_code().unwrap())); + parsed_json["address"]["countryCode"]["prefLabel"]["en"] = Value::String(country.full_name().unwrap().to_string()); + + + //println!("{:#?}", parsed_json); + parsed_json + + +} + diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 56c6e59..da4ad06 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,6 +1,7 @@ pub mod base64_encode; pub mod candidate_value; pub mod desm_mapping; +pub mod elm_mapping_helper; pub mod getters_resolvers; pub mod headless_cli; pub mod init_conversion; diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 1aa5298..82833a4 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -1,6 +1,7 @@ use crate::{ backend::{ base64_encode::create_display_parameter, + elm_mapping_helper::address_to_location, jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, transformations::{DataLocation, DataTypeLocation, StringArrayValue, StringValue, Transformation}, @@ -427,6 +428,54 @@ impl Repository { Some((destination_path, source_path)) } + Transformation::AddressToLocation { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + // run the source value through a markdown converter to fit the nested objects into a markdown string + // let image_individualdisplay_source = Value::Array(vec![json!(create_display_parameter(source_value))]); + let location_source = json!(address_to_location(source_value)); + // let markdown_source_value = json!(image_to_individual_display(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(location_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + _ => todo!(), } } diff --git a/src/backend/transformations.rs b/src/backend/transformations.rs index 9cc4133..4fc42b0 100644 --- a/src/backend/transformations.rs +++ b/src/backend/transformations.rs @@ -193,6 +193,20 @@ impl ImageToIndividualDisplay { } } +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum AddressToLocation { + addressToLocation, +} + +impl AddressToLocation { + pub fn apply(&self, value: Value) -> Value { + match self { + AddressToLocation::addressToLocation => value, + } + } +} + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(untagged)] pub enum Transformation { @@ -236,6 +250,11 @@ pub enum Transformation { source: DataLocation, destination: DataLocation, }, + AddressToLocation { + type_: AddressToLocation, + source: DataLocation, + destination: DataLocation, + }, OneToMany { type_: OneToMany, source: DataLocation, diff --git a/test/ELM_export_example.json b/test/ELM_export_example.json new file mode 100644 index 0000000..fc9c31c --- /dev/null +++ b/test/ELM_export_example.json @@ -0,0 +1,213 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "http://data.europa.eu/snb/model/context/edc-ap" + ], + "credentialProfiles": [ + { + "id": "http://data.europa.eu/snb/credential/e34929035b", + "inScheme": { + "id": "http://data.europa.eu/snb/credential/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "Generic" + ] + }, + "type": "Concept" + } + ], + "credentialSchema": [ + { + "id": "http://data.europa.eu/snb/model/ap/edc-generic-full", + "type": "ShaclValidator2017" + }, + { + "id": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0x7ff3bc76bd5e37b3d29721b8698646a722a24a4f4ab0a0ba63d4bbbe0ef9758d", + "type": "JsonSchema" + } + ], + "credentialSubject": { + "fullName": { + "en": [ + "somebody@gmail.com" + ] + }, + "hasClaim": [ + { + "awardedBy": { + "awardingBody": [ + { + "id": "https://1edtech.edu/issuers/565049", + "legalName": { + "en": [ + "1EdTech University" + ] + }, + "type": "Organisation" + } + ], + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess" + }, + "id": "https://1edtech.edu/achievements/degree", + "title": { + "en": [ + "1EdTech University Degree for Example Student" + ] + }, + "type": "LearningAchievement" + } + ], + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "identifier": [ + { + "id": "urn:epass:identifier:2", + "notation": "student@1edtech.edu", + "schemeName": "Student ID", + "type": "Identifier" + } + ], + "type": "Person" + }, + "displayParameter": { + "description": { + "en": [ + "EBSI Example https://github.com/Knowledge-Innovation-Centre/ESBI-JSON-schemas/blob/main/examples%20of%20credentials/DigiComp%20Generic.json" + ] + }, + "id": "urn:epass:displayParameter:1", + "individualDisplay": [ + { + "displayDetail": [ + { + "id": "urn:epass:displayDetail:123", + "image": { + "content": "iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAznpUWHRSYXcgcHJvZmlsZSB0eXBlIGV4aWYAAHjabVFBDgMhCLzzij5BGVR8jtvdJv1Bn19Uuq2bTuKAjBlB6Xg9H3TrYChJKpprzsEgVSo3SzRMtMExyOCBLK7FtU6nwFaCRcytZj//qcfTYIZmWfox0rsL2ypUv4D1YuQXoXfEluxuVN0IPIXoBq35KFXL7wjbEVboXNQJZXifJte9FHu9PVkRzAcigjGgswH0lQjNkjhY7WBAtVwsDvZR7EH+vVPoP+Pd0vIT+ApL3UFvDxlivyntNoEAAAGEaUNDUElDQyBwcm9maWxlAAB4nH2RPUjDUBSFT1PFIhUHO4g4ZGidLIiKONYqFKFCqBVadTB56R80aUhSXBwF14KDP4tVBxdnXR1cBUHwB8RdcFJ0kRLvSwotYrzhkY/z7jm8dx8gNKtMs3oSgKbbZiaVFHP5VbHvFQGE6IthQmaWMSdJafjW1z11U93FeZZ/3581oBYsBgRE4gQzTJt4g3hm0zY47xNHWFlWic+Jx006IPEj1xWP3ziXXBZ4ZsTMZuaJI8RiqYuVLmZlUyOeJo6qmk75Qs5jlfMWZ61aZ+1z8huGC/rKMtdpjSKFRSxBgggFdVRQhY04/XVSLGRoP+njH3H9ErkUclXAyLGAGjTIrh/8DX7P1ipOTXpJ4STQ++I4HzGgbxdoNRzn+9hxWidA8Bm40jv+WhOY/SS90dGiR8DgNnBx3dGUPeByBxh+MmRTdqUgLaFYBN7P6JnywNAt0L/mza29j9MHIEuzSt8AB4fAWImy133uHeqe27897fn9AJ6icrg0Y35bAAANemlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iCiAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgeG1sbnM6R0lNUD0iaHR0cDovL3d3dy5naW1wLm9yZy94bXAvIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgIHhtcE1NOkRvY3VtZW50SUQ9ImdpbXA6ZG9jaWQ6Z2ltcDowNDE4ODNhMy0zYjAzLTQyNDctYjc2My05MDk4ODYxMGZiNjIiCiAgIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZWNiZWZmYTQtMmVmOS00M2FjLTkxOWUtNTE0MDE5NzBkMjVkIgogICB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6NGI1YmIyNmEtMDRiZi00NDFlLTljOGMtYzZiY2EwZmQ0YWJhIgogICBHSU1QOkFQST0iMi4wIgogICBHSU1QOlBsYXRmb3JtPSJNYWMgT1MiCiAgIEdJTVA6VGltZVN0YW1wPSIxNzM2MzI1NjUxMzI2NjIxIgogICBHSU1QOlZlcnNpb249IjIuMTAuMzgiCiAgIGRjOkZvcm1hdD0iaW1hZ2UvcG5nIgogICB0aWZmOk9yaWVudGF0aW9uPSIxIgogICB4bXA6Q3JlYXRvclRvb2w9IkdJTVAgMi4xMCIKICAgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNTowMTowOFQwOTo0MDo0MCswMTowMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjU6MDE6MDhUMDk6NDA6NDArMDE6MDAiPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6Y2hhbmdlZD0iLyIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NzlmOWRkMy1kNzAxLTQ0ZGUtYmI2Ny1mYmMxZDkzYjIzYmQiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkdpbXAgMi4xMCAoTWFjIE9TKSIKICAgICAgc3RFdnQ6d2hlbj0iMjAyNS0wMS0wOFQwOTo0MDo1MSswMTowMCIvPgogICAgPC9yZGY6U2VxPgogICA8L3htcE1NOkhpc3Rvcnk+CiAgPC9yZGY6RGVzY3JpcHRpb24+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz7aFa8HAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH6QEICCgz6dV/sQAAIABJREFUeNrtnXmUXVWZ6H97n+nOt+akKnMqIQmZKgQkiYYxPgaBIPYKdtOI2srThrYbbEQE7QfKAhTxRX3oWqI+29Y8QSGMCi2BMIkQEhkyBypJkaEy1HSr6t57hr3fH/feU7dSVUlloJHV9a1Vqdxzz9ln728e9v4KRmAERmAERmAERmAERmAERmAERuADD0/+vZK//eSvxbE8a4yg78TBH6/dJs8+c5IwY2aDFUnmLvj893XTKXGeXb1q2GOIETQeH6z4txUinU8J18kgchMTuS7vRqGMa33cP8Tisa/1JsR2tu1i15gt6obvfEWPEOQ9gjeuf0Nu3rVFBuOqtdbOJJ3zrpaevNL18rVKKykQ2jadvG8Hj5tK/MQ7pebZt59t9RvHVYtP3dUUDDWuOYLaw8N/fOopaTVaYteLru588SV9a8+tGqBrrBkLGHORzPLFfK53kdBSukFOaIEQCDRa5PxsxAiMTwRC/o21tuOtaVXWQzG8/wu8A/AvN1wnFj3xdyJ1SVa0bmrSVz2UUgMkZOHChaKrq+u/veRccMH53H33dxXAyts21HW17KoTUadCmEajnRdLvHz+40LLuBvkVfEROeRgGq3R2jJsgdDKikRXB/j/GQTq1UhKtuc988DUyeMPnP5PY3pDxDc1NYm//OUvzJw50xFCJP87E8MwJB//5DIxM3rJUm9D+/XAJBPL0FoJP/CEF7gghDgOle9LIQ3bdJRAolBaaa/TTMbuMQGWLVsmNm/ebM2cOfNzQogbpJTp/94EEcSdhDj9rNGxVWv3SgTSFfk+5BeIcVjQWpfu7Uc1rTVCCFNpRc7LyqIEoQUpo8efYp511lli69atjlLqYSHEOVprIwgC/deOtFgshhACrTW9vb0ndGwhIJv18INAKKWENOQRn9FoylE/FM0OuS7Cf7WWjmVb5u7du3EcZ6nW+hyttVH8+q/ahmiticfjBYIAvdleOIEsJKSB8vIY0jiCMGikMA5BlybQquyKRggDWbyitEIPMlkBAknG3LJli541a9Y3tNbyg+AGW5ZFMpnk4YcfJhaL4Xo+Hz5jCX62c9CFHguYpkl3dzemHUHrgsQMxhS9Xg+7u7azt7uFXr8HQ5jUxEcxqeIk0pHqUGp2tG9lZ+c2QDO1ejZ1iQbQh0qLIOvQbU6bNi2itR5bEv8PEgghEIgTzkUCSU9vN5GUGNI2aK1YvvZr2NJEIkOq6YOa7nd6+czJ1zOhYgoaeKd9I2v2P42r8qQildTFGwZInpRSqPbug7K4MPFBI0T4u1w3nECSeHkXKeXQNkAIokYc0CgCOoNW2v0DIBQJK8qvNv+AbjdTtC6agAAftzDLQbAttECNTvRKPoAwhCSfUKba19ndF1mIoSyIQiDJ626+PG851zV9m0C5CMBVHezNtIQP63AcPZhMa0MaOnDd1g8kQY5AmBMwMHQf3I/hHIbKxXcLQKGxjQiVkRomJuag0Ehhk/V7EUIylAzrfirLwO4JOj/QBHmvNG2gFAcPduIDwjEQUhTwr3VIiEMtvUAghMCSTp93daj8iv6MJMr+L4XUgSPaP9AEKRpDkonYCVVYQRBw8OA+/CBHpMLEVz4duQMczO4j6/ceXlGKMs4XJSKC0H16q5yRhCg8oFDa8USH+UGXDK0VnntiA0M/8LFMj47OLqpPquDl3z3Hg83/G8dIEzESXDf/2wM9CDHU54LjoQVY0uHd7mbMfTZCFGg1vXYuUkid9/O6x+9s++Bne7XGzedO6JBKKRLJCl5/fQMnnzyH7M+yxM06TGESKJdAB0hhIBCooroKKaTLw+8+SyGRSGHyZtuLvHbwaUDTERzgmxX/QcxOaMMytk8e1dT2gVdZ74kaFKC05rHHnmDczFos00EZHgqFJRMY0gABgfLRWhFot88+oAm0T6C9grkRAo1GaY2nXTRgCgdLRInJqpK0CycSe/T8S+r0SD1kiDjEMAy2bdtAIF0WnXEWU3dOJ/ADYmYcISQSwf9suoWcn8UybByjYMwvmLKMM7zzEUKScirQWvHh8eexcOySMonpkyHHimpTmko54oEFa7ZHzVKE/gGLDd9zUErTfnAXzdt3MqGpFnevC1Z/hzXpVJB0KsPPWmuiVoyoFStZdIACsYyhkpEay7Q6bF9v+trP6rNyhBhDmSZFLJbiN/c/TN1J1Xgdfj8J0uVGo8zZ6IuN9GFjplJwaAhTK1P/btao2s5LXq3RIzbkMJDN5XjgNz/DqYFofRRhiAEIHW5sNMR1JaWRdUYl/+2l1eNFwfiPwGE9OAOfHS07mLNsEjooS53rUpZKD+D+8s+DfVe4VggyLdvaHLRn2q3IY3qEIMOAeLKKO+74AY2nNiBjFhJRlpfqkxR9iOoaSjKEEGH11zRsKePG7e35WPB3D12qYWTXyRHB81y2bFrN008/y6KrZvH8vW/2ZZsRw0zhDJLi1SgzYj39N3cveLCfyz3cJJ5hGNi2jRCCIAgIggClFKZphtePJ0kopcS2bQzDQCkVvqP8vccCpedt2x40nX64bIBt21iWRSpZx63/dhuRBotYfRQhy1VPWUyo9aBFMj0wjFe25XQbSftzj122rt+kzOFMzDAMstksjY2NnHzyyUyZMgXTNMlkMrz55pu8/fbbtLW1IaVEKXXUaRDLsujt7aWuro5JkyYxe/ZsYrEYPT09rF+/nvXr1+N5HoZhHBOxS8T0fX/YRFFKhevJ57P4QQ//7z9W8Ol/+jSP3PgnDEv2SxIOJim6VG7UOlRzAoEUQhmWsZy7Tm25CKmHTZDShC666CK+8IUvUF1djeM4IWK01vi+Ty6XY8uWLdx44420tbWRz+eHxdFCCKSUnHfeeXz2s59lzJgx4fglFzIIAnp7e2lubuaOO+5gx44dwyaE67rceeedLFlSCMoee+wxbrvtNizLOuyzjuOwYMECvvWtbwGwbt06PvOZz/LL3/yERWcuZsHV03nphxswonLQdZYUVHkxq2/NMojEok/XNMZvf/KrAu48BOeHE/UDBw5w7733cuuttzJ27Fii0egADjNNk0Qiwfz583n44YdZvHgxhmEcsVZhmib79+/nG9/4BrfffjtTp04lFov1k4KSdKZSKebMmcN9993H+eefPywpLBE0FouRTqdJp9PEYrFh1VCEEDiOQyqVoqKigng8jue5qEDw6c9fQ3pqnPpTqxCGGGrDwhCWRGvbcvYmZ1Qu2/6q8u66Uwx42BxKMgzD4PHHH2fevHnhIrZs2cK6devYvXs3PT091NTUMH78eJqamhgzZgzRaJS77rqL73//+/z7v//7YfW64zg88sgjnHrqqaHn0drayl/+8hdaWlrYu3cvpmkyduxYGhsbmTdvHvF4nK9+9atUVVUdEbH9dPwxBL+hOip7Lgh8UrE8t95+J7d//RZe+dU29r/VBgFHTP9rrVXEiR5wyVweeTjf/dn/XDIoV5lD2Yy77747JIbnefz4xz/mO9/5DqNGjeo3Sdd12bp1Ky+++CJz587Ftm2+9KUvsXr1anbv3o3neYNO8LrrruO0004L37lu3TouvPBCampqiEQi/e7dv38/M2bM4Ec/+hETJkzgH/7hH5BSvi8Zhmw2xyvP/YHbvhPj1htv4Mnb15LvyA8mDWXBo1ARO9Ju10Quer4989pVQxBjgMoqIf/yyy/nzDPPRGuNUop77rmHFStWkE6nyeVyZLPZ8CcIAqZOncqVV17JG2+8gRCCSCTC9ddfPygxIpEIp59+Opdddln4zhUrVvD5z3+euro6tNb9xs9msyQSCfbv388VV1zB5s2biUajA4j2XwluoHjxjyu55bb/xQXfOIUJZ9ejA90vWxK6xAhf2sY+lTY/Yk48bc29P/rYYfXtABvS3d3N0qVLQ+5buXIlv/rVr8hkMkMOEgQBtm1z0003kc1m0Vpz6qmnYllWP5sjpaS7u5svf/nLWJaFEIL29na+/e1vh+MMzZkF4lx77bX09PS8r1uWBIK8F/DCs09yzT9+mbkXT+Cki8bjZXzKSujKlKayHee1WDIxRz39+palV4sjTrofQeLxOIsXL2bKlCkIIejo6OB73/sepnnk+DEIApqbm2lra0MIQTqdZtmyZdi23U8f19fXM27cuNC1vO+++0KpOJKhzefztLa28otf/OKvYg+ZHyi2vL2WT171BWpOi/LRb8zHqXQgENq2nTZhye/2Vvof9WLRtivXfXFY8YAsV1fZbJbzzjsvJMCaNWtobW3F8zyklIf90VqTSqXYuHFjON6ZZ55JPp8PpSQIAubPn080GgWgo6ODBx54AKXUsN1kwzB44okncF33fSeKEILe3l72vbuJj5w6h9+vXcXSby5QjRfUb3SSTpPZvPPGlsjZ3X/7zdnBcMc0ywc/ePAgU6dODT93dXUxb9680ICWI6B/XVuH/vu+ffvC6w0NDWQyGdLpNL7vY5omM2bMCMfas2cPHR0dIYGGi4R3332X7u5uqqur/wqSKwLPzVNXP56f/PA7rH35Ra7952ufzNjNuy/87qc0T33qqEYzD41Oq6qqws+XXnopS5cuPWquKTfg5deEEDQ2NoZuaEtLC4lEAt/3hz1+EAT09PTQ2tr6V0KQPr8qn8/zp5ee5fnV/5mLxqPHNIo5WAxSHi8cjziXjHq5dJVsSskmHE1+qXyOh3MATgQcy7wK2kJhOxGix+gFmoNxYAl+//vfs27dumPy94UQZDIZHMfB9/1wjFwuF/4/Go0SBAFHu9G7pP7eKyQHQUAkEjnGOEegtUJK8/gJIqWko6MjnEhHRwe//vWvw8MxRwtKqQFplJ07d4YEaGhoIJvN4jjOUSE1kUhQW1s7bCKW5j5cgnieRyKReF8Un1lumGtra1m/fj0LFy5Ea83kyZPD4O941FcJKb7v88Ybb4SIHDt2LJWVlWHsMtyUxuTJk0mlUkd8XzkTKaWoqanBdV1s2z7ss93d3cyaNeuY1dZxqcryBUQiEf74xz+GbmhTUxONjY0kk8lhIyuTyfTLIx2aw3r99dfJ5/Ohm3zxxRdjmuawCFK656yzzhpWjcSyLHK5vk109fX1tLa2HhHRO3bsoKmp6ahLCSeUICXOeOmll3jrrbfQWmPbNrfddhvNzc1HXLwQAtM0+elPf8pXvvKVQT0nrTV79+5l27ZtYfxyzTXXYNs2sVjsiMSIRCI0NjZy9dVXD2txyWSSLVu2hPMbN24cl1122WHT747jsHz5csaNG/f+Skhp0qlUioceeii8tmDBApYvXx5W3AarEZcmfu6557J48WKuuuoqvva1rw1YuFKKWCzG9773PXzfR2tNRUUF//qv/4rrukMmDEvVxGQyyT333DNsm+Z5Hk899RSu64YSeskll+C67qDPG4ZBZ2cnF198cb9M8fATmQKNpJA/CY6PIOXpjccee4yVK1eGxvfyyy9n+fLlVFVVhUa19GMYBvl8nrvvvps777wTx3HQWjN+/Hiy2eyAhWSzWV555RVWrFgRXrvsssu4//77icfjhX21ZePH43Gy2SxLlizh/vvvZ+LEiWSzWfL5/LAI0tzczKpVq8I1Ll26lEsvvRStNYlEglgsRjweJxqNUltby6OPPsrEiRPp7OwMPc7SGo6sVnVxt6/mWPv6iGnTpkUsy9oHxIsHP8N6yM9//nOamprCiXR2dvLnP/+ZV199ldbWVtLpNIsWLWL+/PmMGjUqvK+5uZlly5bhed6g8YJhGOH4c+bMCa/39PSwceNGXnjhBZqbm7Ftm5kzZ3LGGWcwceLEMEXz4x//mCuuuIJkMonrunzkIx8J7dKhYNs2tbW1PPjgg6Fk+b7PmjVreOihh9i2bRsVFRUsWbKECy64gFQqhdaab33rW9x8882h3fvYxz5GOp0ejhpTUsq70un0zS+88II+IQQpESUajXLrrbdy9tln4zhOWGcezL0tIesPf/gDd9xxB5lMZshaSIko0WiUG264gaVLl2KaZuhMHCpVpetdXV388pe/5IEHHuDRRx8lHo/jui6LFy8ekiBaayzLYunSpdx8882hGi29o3zuWmtyuRz33nsvzz//PL/97W+RUrJ161bOOeccKioq3nOCmIeLIXK5HNdddx0LFy7kpptuCsu4h+a0PM9j586d3HfffaxcuZJYLHZYYpSCr97eXr7+9a/z9NNPc80119DY2EgkEhkwfm9vL5s2beLOO+9kx44dRCKRMAVfes9Q6qQkEY888ggtLS3ccsstjB8/PvTsSmrZ8zw2b97MLbfcwu7du4nH43R1dYUBqOd5/yUFsTIJ0XGtB9ZHSt5NW1sbU6ZM4eKLL2by5Mnhdp1NmzbxzDPPsHXrVioqKujp6TnqScRiMbq6upg0aRLnnHMO06dPxzRNgiBg48aNPP744+zfv59IJBJuoIhGo0fdySEajZLP55k7dy5Lliyhrq4OgL179/LEE0+wZs0aqqqqQvsUj8cLBSnXDZ2Q4cTDx6Wypk+bFjEsax+IOFrLoU4Xl0S15N6WJleaqBDiuPx2KWVY9ixPi5S7z+XjlweqR5PXKqkn0zT7qa3SFqFD31Fa51Gs7fhUlu63GVIjhURiEOgApYPi0V+B0AYCiadcPN/vO4Va1Pnl3FMYwywiuOAO+todfALCLu6PDVBaobTCD3wMChXFQAeDqorBiCAQGMIi0B6B9pHCwBAmIAi01w+x2ivf/imxhAMayvZT46tDay4aQ1igwcdFazClhcTovz4h4BiZ0zxUIiwRpSE2maSVImLFCse4dEC328me3l3syW3DkQ7ltcpDkWIJh9HRyViGTaB8ur0u9uWa+x8RLkKV3UDaqSRqxoiaMaQw8JVLxs2wp3cH+/LbsUUEiTFk6wyBwNceCs3k2HRqo3U4ZgxP5enMt9PS8zY5v7uAdAqdGmojE46gggQH3XfxyIXPeDpHxEgxMTGNikg1aMi4HSFeTGEWGEBrOMag0iynPmgWjPooH57wUQIVFDi8WLsvHBbV7O7ayfMtj7O9Z/OgO19cneeSiZ9hRs28IgILknfv2lvpDbpQOgilSGvNp2b/E0ZZZlSXzugJgdIB+3v28Oddq9jQuaa40XngOQuF4rTaJZzWcGYBUYWOYeH+tEAFvNn6Cn9o+TWWcLBliqvm/nN432BqWgjJM82P8nLrk0hRaAPzP8b+LfPqF2EW59u3MVHTkWvjuZYn2Nyx9kRF6iJEX6CCsvNzAUr7BKqwl3dUYgyfmP45kmZVsRNOf8ipHsalGvGUiyqqIYHk1FFnDeBAEKGaCn8ICLRfUJcaauP1XHTS3/OpGdcTaO+QneUSX3v83bR/5txJS0naaZQOCnMt/g5UgBSSuJ1EI4uoV4VzgMX7Su8ufC7NJSBmxQFBJujkb6ddy/yGjyCFDO8Nx0BTGa3GlpGiijxBqZMS1cs5Jh9k6Xa7Qs4UCAxpceGkTxLo/vkqKQzmVpxBzIoXuapo5XTA6PgYev3OQWJbHcYAWmskoqD7pRnOQ6OoT0zg441X0xv0hATNBhkunfw5xqUm98UWWmMaZsGOCYkhC8Z7R+c2+h23OUQoDGliGhaGNDFkwfZ4yscQJotqL2R8urEPL0IQaB9X5Ql0gCEkXuDyVvuf8LV3YtLv5Xwb5qiE5M3WNTyw84fMTy5h2czPh2isjTfg6zy2iIbG29ceH6o/K1SA5Z0KqmKjyGmf6AC10+cUKBQ/e/0eTGkwMTWD0xoWE7XiFJpKKk6qns3UvXPZk90OaCbGZjKjdh5K9Rn+jNvJy+8+y/bMRoSAsfEpTK6Yzkv7niIi7TIXpo8hAuXz3Pbfh/23SnZ5c/vr5FUvJ1XNItCK0lG2vV0t3Lf+dqTUxGSak9KnUBurp9vvImbEOZ4uBiZDUaQoLYYwqTXH0JLdgq88LMMKbUNoXIpNunJ+nprYqNB97cwdJO1UodHErQQzU/PZm90xQLLC92rI+AcBTdv+vbR0vcMVs/+xuIu8cNZ7YcMSfrPthwgkTXULw03NJYL+fP130cpHiYKXs6HjVda1PYMjI4M7BbpgY1a3riRmJAeUYxU+hjDCI5wCQXvuAAiIyCQa2Nz5Gm+254gbiePu2SX7XNa+o1r9tTwIYXPe+MuxDLvgbwiDHR3bsGQUjQqpV2VX45iFSF4Aq3c+jkKFfa0WNCwpup9iIP11n6vpKw9fe7RkN/Byyypk8XmlFaOT48ipgE4/w8SKaQSqLwH45t5X6PJacXWuOI6L0j6WiByxdDA10cT42HQmxKczIT4DS0QKKUJhEmi/H6Jn1M5lWeMXiRtpPJUL7zsRDdTM/huRdT8JUSjm1i9gbv3pSGEWvRJN1svw+PZfFVVJMUDE50PVi5DCKLS7UC5vtb3GmfmLSEcqUVrRkJyApxWOMI6QMy3WJmSULR1vsWjcErQuEN4ybGLSwRKaiNXX40QKg51dbxMz0qEEuipLZ9CDKRS+FiRlnIRphypJF7u62YbDJ2d+MVyPIU2eaX6UtftXY2CwvWMbU6tn4Ss/dIGn1cxlavVsdnVtZ9XOlbT0vk1EOsdNlGEd2NGa0E4IIfACl2pnNG3unqKbKkArZo/6EBQloq3nAL7IcqB3DxWRKhCaiBllSmIm23s2YhTT0/oI9YUutxOlA0TRTZZCkjArikGlSXkLi063HVH0UwxhsqD2AhaOO7fozQVsPLCOVbsewiZOeQjSdxKqr1tchVNNoAM0Pn/a/zgNyfHMqG0iKLNXAsH4dCNXzvoXNh94g9++83+IyMSJ9bJCzaX7vCpZ7FxQ+i4VqeTKmV8iIpPF7w2q7XFURmrQRW7dlWkmZVRyoLe1aGcKi5g36sPFhR6issRAG1Zq3NKvFVLR91dh3UEPbgARRK0ECTtF1IwRt5LEzSRK6yHq7wWvrKReM15nIVYCbBHj/rd/wKu7nivGUaLPGSnay5l18zl/7JUI5HE1HRzUqJckQSB5o/XPvLZvNVXOKM6acBEpp7LwoGHx8SlX8bONdyEQnNFwcb8t+G3Z/YBgW8dbfGjs2aAL3zUkJyDQGBgE5VW1Qc7ba62otCuRyH7c2+V1klcZfO1jYoV0qI7Wsi+/fWAcX5TyPmHqv+vSUy6/3XQflrDCY2d7szvDdItGEZEJVu16kFW7HmRKajYLG5YwOjkuJGigfGbVncpjLSuoNFInVmWVM5EXeOzN7uBAbjfOu1EumLIsbHVaE6/Hlg7ZoJfJlTP6kow64NzJSzl70kUhl5XEPGYnSJmjcXUP6GAoB68YZOaYWX1KP72cD/Jkgyx57dPrdocMorRiauVMXjnwNHEjPlBe+n0YeHZ8S+Y14kZqUJe1xPWqmNfb1vU6GzvX8OG6j3HWxI+F87OkiVUMDIPjLeH21wllJUkhsKSNIezQsPapM4klLCqtGlJORb+OOLrIkqqsC5tGI5HMqj4FV2UHOSncF0hm/AxzqxYyZ/SC8BspJHu6dxKRFpVmirfbNhU68xSfnlI9kzkVC8kEXSjth96ePpypLc4hG+TIBB1kgvbwx9cuAkGFXUvSqqTTP4in88U5wsFca9hHsaRawzPswQmTkP6H3k1hUmE2UBMdxcKxHy1kf4svzgc5PO0yPX0ajhkJXVAvKMQrJVWT93NEzGjouk6rnsvTe35HsmicSxIktGB+7dk4Msro5FgmVpxU9HxE6OE92/IYlix4Sq/ue4ZTGhaFzSgFmo9P/zTzOxazN9NCbbwBVVSjekh6CEzD4nPTbw7b9JUijnc6NrD2wHMsHnMh02rmsKtrB7u6mun1e6iMVDO9pgmtdBiLuUG+QEQd79eG44R5WYEKmDP6Q8ypP73Q7FGr4lnsgqV4a9+rZPx2ptfMDYkhheThTb+kzduLKU3yQY6Dbis3nba8kOcRUBdvYGx0Chmvvd+Reikk505aig7Ltv3VxvoDa9md3YIjCyppf34n6/a8xLz6ReW5ASakG5lYcVKRiKo8d9qPECW5MYTBlKqT+32HEFiGzbOtvyMdKQS3DcnxjE1NKtqkQs6rz+2WrN/3GgkjXkj9H2OTDDmUVdVhGk6hVFDwjHQhOyqR7OjYynN7HsExYjSkJoS5qB6vm209a8l4bbTl99Ljd5BTPbR27yorEhnMqV7Qp8pKuSx0kegqlIiSNX5193M83PzTkBiFOCXGUy2/YfWOx4vxjw4TCKWkYd9hfn2Is9Y/h1auHEr3S2GQ10Gx71WYliRQfoEYpW1CQrJx3zqe2rXiSAryaCSkwDM5P0vW6ykUXqSBIQw0Cl8HuH6OjNtJc/sm/rjrN8SMNJOTswoEE4XM5zttG/G0hymscIVJI8X2ji3UxEZjCIO8zlIdraMnyNPtdhIzExiGiYGJFAJf+fjKp8ftoiN3kJd3r2J7z3qsYjGrX1JQGLzc+iQHs63MH72Y6tgoomYMQ5porfC1T84tJEibOzZjCgNFQHe+lDAVg6ffEezv2UOlWcm2tg3UJ8aRciqwTQdDFmpEXuDSkTvItrYNPLvndzgy3ieRx19TL+w6cVUOT+dImjXEzSRRmcbXeTJ+G53efvI6IGHEsEWk0NNZB3SrdlKyFl/nCfCIyuQAxHk6h69dIiJNXmcItE/CqKDd30/cSJMw08SNNKaM0O23kfHa6QraMImQMtN9KZohA1hJp9cOQlFljiJmplDaI+N30OXvQ2OTNBKFih/Q5u3t87i0KGsm0yclMZkiIhNkVS+5IEvMiFJljyZupMmpbtrdfWSCDAkjiSWKOT6tlWHIu5LpqptffOG54991YssIDjEEkA16yPm9IAq6OG5UECsipsRdhjCpMOoQwsDAKJZ9B87DEhEcWcjcGqSKrjNUmLXFKqFPRrUVM6oKWzpUitqiAjgy12mtSJqp0JHoDboAjSksKszRocsaViqt+mGgp6C6ojJKVEYLKizoxQ1yaKGxpUNamKEnB6CliSulkyk2AAABZUlEQVQriATrTmAcgiqkmwdvjjZETePIp6CUDg5zfXDP/Wii3pKhDgoF8yOMc/R6XukAVZqlHjx2EjrAVm3kjCbg+RMRh4zA8UKhzHZstmSEIO8RSY61SDVgs/UIvL9Qqofo93Kb5PvV+fT9eG+x2Zs6mpPF/QiyZcuW3OzZs9/VWk9/ryf6viiP4+gIdKwljSAI3lFKiWPxHOTZZ58thBA3CyECTuhfqfnrgv8iYmghRCfwiGEYx4RLs7jP9QnXdR8RQlym+/YBjRiVo6K3QAjhKqX+Pp1Ot7344ovHhD8B8IlPfEI0Nzebnud9Vmv9OcMwRjPyh4uH5U1pIyII3ECpYI2AH3R1dT27Y8eOY2bmEOkLFiwQWmuZzWaFUioihBAjnteRXdtgfJMwDr4bdHVlshNr4zz33HPHlcwa+ePExwtqG7ZoZN2GDWoEGSMwAiMwAiMwAiMwAiMwAiMwAiNw3PD/Ad0JVZHNnanPAAAAAElFTkSuQmCC", + "contentEncoding": { + "id": "http://data.europa.eu/snb/encoding/6146cde7dd", + "inScheme": { + "id": "http://data.europa.eu/snb/encoding/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [ + "base64" + ] + }, + "type": "Concept" + }, + "contentType": { + "id": "http://publications.europa.eu/resource/authority/file-type/PNG", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/file-type", + "type": "ConceptScheme" + }, + "notation": "file-type", + "prefLabel": { + "en": [ + "PNG" + ] + }, + "type": "Concept" + }, + "id": "urn:epass:mediaObject:https://avatars.githubusercontent.com/u/22613412?v=4", + "page": 1, + "type": "MediaObject" + }, + "type": "DisplayDetail" + } + ], + "id": "urn:epass:individualDisplay:c05743e7-9f9d-4e0b-899b-7ae6514c7a02", + "language": { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "notation": "language", + "prefLabel": { + "en": [ + "English" + ] + }, + "type": "Concept" + }, + "type": "IndividualDisplay" + } + ], + "language": [ + { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "notation": "language", + "prefLabel": { + "en": [ + "English" + ] + }, + "type": "Concept" + } + ], + "primaryLanguage": { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "notation": "language", + "prefLabel": { + "en": [ + "English" + ] + }, + "type": "Concept" + }, + "title": { + "en": [ + "DigiComp Generic" + ] + }, + "type": "DisplayParameter" + }, + "id": "http://1edtech.edu/credentials/3732", + "issuanceDate": "2010-01-01T00:00:00Z", + "issued": "2010-01-01T00:00:00Z", + "issuer": { + "id": "https://1edtech.edu/issuers/565049", + "legalName": { + "en": "1EdTech University" + }, + "location": { + "address": { + "countryCode": { + "id": "http://publications.europa.eu/resource/authority/language/USA", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/country", + "type": "ConceptScheme" + }, + "notation": "country", + "prefLabel": { + "en": "the United States of America" + }, + "type": "Concept" + }, + "id": "urn:epass:certificateAddress:1", + "type": "Address" + }, + "id": "urn:epass:certificateLocation:1", + "type": "Location" + }, + "type": "Organisation" + }, + "type": [ + "VerifiableCredential", + "VerifiableAttestation", + "EuropeanDigitalCredential" + ], + "validFrom": "2010-01-01T00:00:00Z" +} \ No newline at end of file diff --git a/test/Quick_test.md b/test/Quick_test.md new file mode 100644 index 0000000..f8f2282 --- /dev/null +++ b/test/Quick_test.md @@ -0,0 +1,4 @@ +example: +```sh +cargo run -- -i ./test/OBv3_example.json -o ./test/ELM_export_example.json -m ./json/mapping/custom_mapping_OBv3_ELM_latest.json -c OBv3toELM +``` \ No newline at end of file From a59a95acf8f123fd4a8701d9d527cfa6060f20a9 Mon Sep 17 00:00:00 2001 From: hamrt Date: Fri, 31 Jan 2025 18:29:37 +0100 Subject: [PATCH 34/45] updates for testing and api --- README.md | 15 +- .../validation-schema-ELM3.2/README.md | 12 + .../validation-schema-ELM3.2/schema.json | 2998 +++++++++++++++++ .../custom_mapping_OBv3_ELM_latest.json | 12 + src/backend/base64_encode.rs | 19 +- src/backend/elm_mapping_helper.rs | 13 +- src/backend/init_conversion.rs | 2 +- src/backend/routes/api.rs | 148 + src/backend/routes/mod.rs | 3 + test/encoded_test.json | 6 + 10 files changed, 3216 insertions(+), 12 deletions(-) create mode 100644 json/ebsi-elm/validation-schema-ELM3.2/README.md create mode 100644 json/ebsi-elm/validation-schema-ELM3.2/schema.json create mode 100644 src/backend/routes/api.rs create mode 100644 test/encoded_test.json diff --git a/README.md b/README.md index f5e7fee..ef2be23 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,22 @@ cargo run -- -w 192.168.1.1:5000 A webpage displaying a form can be found at the root of the project a webservice api can be found at /translate_file you could use the website to translate the files by surfing to 127.0.0.1:3000/translate_file and providing information in the form presented. -An other option is using the API directly: +An other option is POSTING to the page directly in a multipart format: ```curl 127.0.0.1:3000/translate_file -F translation=OBv3ToELM -F input_file=@test/OBv3_example.json``` +There is also the option to POST direclty in json format: +```curl 127.0.0.1:3000/api -H "Content-Type: application/json" --data @test/encoded_test.json``` + +The data format for this json is: +```json +{ + "From": {"Name": "OB", "Version": "3.0"}, + "To": {"Name": "elm", "Version": "3.2"}, + "Parameters": { "PreferredLanguages": ["en", "sv"]}, + "Content": "Base 64 encoded content in From format" +} + +``` ## Usage diff --git a/json/ebsi-elm/validation-schema-ELM3.2/README.md b/json/ebsi-elm/validation-schema-ELM3.2/README.md new file mode 100644 index 0000000..6510d9a --- /dev/null +++ b/json/ebsi-elm/validation-schema-ELM3.2/README.md @@ -0,0 +1,12 @@ +# Schema as proposed to use in the DC4EU project. + +> The schema defines a generic structure for used within EBSI-related Verifiable Credentials according ELM +> This schema is also used to validate DC4EU + +The schema is published to the [Trusted Schemas Registry](https://hub.ebsi.eu/apis/pilot/trusted-schemas-registry) with the IDs: + +- `0xbe77a21356835dc09d3d8149ea832ae0a4bae0ae9c869d18219ef8f4a74b4644` (hexadecimal) + +# validation of the outcomes of the converter can be done by importing this schema into on-line tools like: +[json schema validator](https://www.jsonschemavalidator.net/) + diff --git a/json/ebsi-elm/validation-schema-ELM3.2/schema.json b/json/ebsi-elm/validation-schema-ELM3.2/schema.json new file mode 100644 index 0000000..48e9f54 --- /dev/null +++ b/json/ebsi-elm/validation-schema-ELM3.2/schema.json @@ -0,0 +1,2998 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Europass EDC credential", + "description": "Schema for EDC credential based on ELM 3.2", + "type": "object", + "allOf": [ + { + "$ref": "https://api-pilot.ebsi.eu/trusted-schemas-registry/v3/schemas/0xbe77a21356835dc09d3d8149ea832ae0a4bae0ae9c869d18219ef8f4a74b4644" + }, + { + "$ref": "#/$defs/EuropeanDigitalCredentialType" + } + ], + "$defs": { + "CredentialSubjectType": { + "$ref": "#/$defs/AgentOrPersonOrOrganisationType" + }, + "IntegerType": { + "type": "integer" + }, + "PositiveIntegerType": { + "type": "integer", + "minimum": 0 + }, + "PercentageIntegerType": { + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "DecimalType": { + "type": "number" + }, + "BooleanType": { + "type": "boolean" + }, + "IRIType": { + "type": "string" + }, + "URIType": { + "type": "string", + "format": "uri" + }, + "Many!HTMLType": { + "anyOf": [ + { + "$ref": "#/$defs/HTMLType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/HTMLType" + } + } + ] + }, + "HTMLType": { + "type": "string" + }, + "DateTimeType": { + "type": "string", + "format": "date-time" + }, + "EmailType": { + "type": "string", + "anyOf": [ + { + "format": "email" + }, + { + "format": "uri", + "pattern": "^mailto:[^@]*[^\\.]@[^\\.]($|[^@]*[^\\.]$)" + } + ] + }, + "DurationType": { + "type": "string", + "format": "duration" + }, + "Many!PeriodOfTimeType": { + "anyOf": [ + { + "$ref": "#/$defs/PeriodOfTimeType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/PeriodOfTimeType" + } + } + ] + }, + "PeriodOfTimeType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "PeriodOfTime" + }, + "startDate": { + "$ref": "#/$defs/DateTimeType" + }, + "endDate": { + "$ref": "#/$defs/DateTimeType" + }, + "prefLabel": { + "$ref": "#/$defs/Many!LangStringType" + } + }, + "required": [] + }, + "Many!StringType": { + "anyOf": [ + { + "$ref": "#/$defs/StringType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/StringType" + } + } + ] + }, + "StringType": { + "type": "string" + }, + "GenericIdType": { + "$ref": "#/$defs/URIType" + }, + "LiteralType": { + "$ref": "#/$defs/StringType" + }, + "Many!AgentType": { + "anyOf": [ + { + "$ref": "#/$defs/AgentType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/AgentType" + } + } + ] + }, + "AgentType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Agent" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "altLabel": { + "$ref": "#/$defs/Many!LangStringType" + }, + "prefLabel": { + "$ref": "#/$defs/Many!LangStringType" + }, + "location": { + "$ref": "#/$defs/Many!LocationType" + }, + "contactPoint": { + "$ref": "#/$defs/Many!ContactPointType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "groupMemberOf": { + "$ref": "#/$defs/Many!GroupType" + }, + "dateModified": { + "$ref": "#/$defs/DateTimeType" + } + }, + "required": [] + }, + "Many!PersonType": { + "anyOf": [ + { + "$ref": "#/$defs/PersonType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/PersonType" + } + } + ] + }, + "PersonType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Person" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "location": { + "$ref": "#/$defs/LocationType" + }, + "nationalID": { + "$ref": "#/$defs/LegalIdentifierType" + }, + "fullName": { + "$ref": "#/$defs/LangStringType" + }, + "givenName": { + "$ref": "#/$defs/LangStringType" + }, + "familyName": { + "$ref": "#/$defs/LangStringType" + }, + "birthName": { + "$ref": "#/$defs/Many!LangStringType" + }, + "patronymicName": { + "$ref": "#/$defs/Many!LangStringType" + }, + "memberOf": { + "$ref": "#/$defs/Many!OrganisationType" + }, + "dateOfBirth": { + "$ref": "#/$defs/DateTimeType" + }, + "placeOfBirth": { + "$ref": "#/$defs/LocationType" + }, + "citizenshipCountry": { + "$ref": "#/$defs/Many!ConceptType" + }, + "gender": { + "$ref": "#/$defs/ConceptType" + }, + "contactPoint": { + "$ref": "#/$defs/Many!ContactPointType" + }, + "groupMemberOf": { + "$ref": "#/$defs/Many!GroupType" + }, + "dateModified": { + "$ref": "#/$defs/DateTimeType" + }, + "hasCredential": { + "$ref": "#/$defs/Many!EuropeanDigitalCredentialType" + }, + "hasClaim": { + "$ref": "#/$defs/Many!ClaimNodeType" + } + }, + "required": [] + }, + "Many!EuropeanDigitalCredentialType": { + "anyOf": [ + { + "$ref": "#/$defs/EuropeanDigitalCredentialType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/EuropeanDigitalCredentialType" + } + } + ] + }, + "Many!ClaimNodeType": { + "anyOf": [ + { + "$ref": "#/$defs/ClaimNodeType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/ClaimNodeType" + } + } + ] + }, + "ClaimNodeType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningAchievementType" + }, + { + "$ref": "#/$defs/LearningActivityType" + }, + { + "$ref": "#/$defs/LearningAssessmentType" + }, + { + "$ref": "#/$defs/LearningEntitlementType" + }, + { + "$ref": "#/$defs/ClaimTypeNodeType" + } + ] + }, + "Many!OrganisationType": { + "anyOf": [ + { + "$ref": "#/$defs/OrganisationType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/OrganisationType" + } + } + ] + }, + "OrganisationType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Organisation" + }, + "dcType": { + "$ref": "#/$defs/Many!ConceptType" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "altLabel": { + "$ref": "#/$defs/Many!LangStringType" + }, + "homepage": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "location": { + "$ref": "#/$defs/Many!LocationType" + }, + "accreditation": { + "$ref": "#/$defs/Many!AccreditationType" + }, + "eIDASIdentifier": { + "$ref": "#/$defs/LegalIdentifierType" + }, + "registration": { + "$ref": "#/$defs/LegalIdentifierType" + }, + "legalName": { + "$ref": "#/$defs/Many!LangStringType" + }, + "vatIdentifier": { + "$ref": "#/$defs/Many!LegalIdentifierType" + }, + "taxIdentifier": { + "$ref": "#/$defs/Many!LegalIdentifierType" + }, + "logo": { + "$ref": "#/$defs/MediaObjectType" + }, + "hasSubOrganization": { + "$ref": "#/$defs/Many!OrganisationType" + }, + "subOrganizationOf": { + "$ref": "#/$defs/OrganisationType" + }, + "hasMember": { + "$ref": "#/$defs/Many!PersonType" + }, + "groupMemberOf": { + "$ref": "#/$defs/Many!GroupType" + }, + "contactPoint": { + "$ref": "#/$defs/Many!ContactPointType" + }, + "dateModified": { + "$ref": "#/$defs/DateTimeType" + } + }, + "required": ["legalName", "location"] + }, + "MediaObjectType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "MediaObject" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "contentType": { + "$ref": "#/$defs/ConceptType" + }, + "attachmentType": { + "$ref": "#/$defs/ConceptType" + }, + "contentEncoding": { + "$ref": "#/$defs/ConceptType" + }, + "contentSize": { + "$ref": "#/$defs/IntegerType" + }, + "content": { + "$ref": "#/$defs/StringType" + }, + "contentURL": { + "$ref": "#/$defs/URIType" + } + }, + "required": ["contentType", "contentEncoding", "content"] + }, + "Many!AccreditationType": { + "anyOf": [ + { + "$ref": "#/$defs/AccreditationType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/AccreditationType" + } + } + ] + }, + "Many!IssuerNodeType": { + "anyOf": [ + { + "$ref": "#/$defs/IssuerNodeType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/IssuerNodeType" + } + } + ] + }, + "IssuerNodeType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "IssuerNode" + }, + "eidasLegalIdentifier": { + "$ref": "#/$defs/LegalIdentifierType" + } + }, + "required": ["eidasLegalIdentifier"] + }, + "AccreditationType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Accreditation" + }, + "dcType": { + "$ref": "#/$defs/ConceptType" + }, + "identifier": { + "$ref": "#/$defs/IdentifierOrLegalIdentifierType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "homepage": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "dateIssued": { + "$ref": "#/$defs/DateTimeType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "decision": { + "$ref": "#/$defs/ConceptType" + }, + "report": { + "$ref": "#/$defs/WebResourceType" + }, + "organisation": { + "$ref": "#/$defs/Many!OrganisationType" + }, + "limitQualification": { + "$ref": "#/$defs/QualificationType" + }, + "limitField": { + "$ref": "#/$defs/Many!ConceptType" + }, + "limitEQFLevel": { + "$ref": "#/$defs/Many!ConceptType" + }, + "limitJurisdiction": { + "$ref": "#/$defs/Many!ConceptType" + }, + "limitCredentialType": { + "$ref": "#/$defs/Many!ConceptType" + }, + "accreditingAgent": { + "$ref": "#/$defs/OrganisationType" + }, + "reviewDate": { + "$ref": "#/$defs/DateTimeType" + }, + "expiryDate": { + "$ref": "#/$defs/DateTimeType" + }, + "landingPage": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "status": { + "$ref": "#/$defs/StringType" + }, + "dateModified": { + "$ref": "#/$defs/DateTimeType" + } + }, + "required": ["title", "accreditingAgent", "dcType"] + }, + "Many!QualificationType": { + "anyOf": [ + { + "$ref": "#/$defs/QualificationType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/QualificationType" + } + } + ] + }, + "QualificationType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Qualification" + }, + "dcType": { + "$ref": "#/$defs/Many!ConceptType" + }, + "identifier": { + "$ref": "#/$defs/IdentifierOrLegalIdentifierType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "homepage": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "altLabel": { + "$ref": "#/$defs/Many!LangStringType" + }, + "category": { + "$ref": "#/$defs/Many!LangStringType" + }, + "dateModified": { + "$ref": "#/$defs/DateTimeType" + }, + "language": { + "$ref": "#/$defs/Many!ConceptType" + }, + "volumeOfLearning": { + "$ref": "#/$defs/DurationType" + }, + "mode": { + "$ref": "#/$defs/Many!ConceptType" + }, + "learningOutcomeSummary": { + "$ref": "#/$defs/NoteType" + }, + "thematicArea": { + "$ref": "#/$defs/Many!ConceptType" + }, + "educationSubject": { + "$ref": "#/$defs/Many!ConceptType" + }, + "creditPoint": { + "$ref": "#/$defs/Many!CreditPointType" + }, + "educationLevel": { + "$ref": "#/$defs/Many!ConceptType" + }, + "learningSetting": { + "$ref": "#/$defs/ConceptType" + }, + "maximumDuration": { + "$ref": "#/$defs/DurationType" + }, + "targetGroup": { + "$ref": "#/$defs/Many!ConceptType" + }, + "entryRequirement": { + "$ref": "#/$defs/NoteType" + }, + "learningOutcome": { + "$ref": "#/$defs/Many!LearningOutcomeType" + }, + "influencedBy": { + "$ref": "#/$defs/Many!LearningActivitySpecificationType" + }, + "provenBy": { + "$ref": "#/$defs/Many!LearningAssessmentSpecificationType" + }, + "entitlesTo": { + "$ref": "#/$defs/Many!LearningEntitlementSpecificationType" + }, + "awardingOpportunity": { + "$ref": "#/$defs/Many!AwardingOpportunityType" + }, + "hasPart": { + "$ref": "#/$defs/Many!QualificationType" + }, + "isPartOf": { + "$ref": "#/$defs/Many!QualificationType" + }, + "specialisationOf": { + "$ref": "#/$defs/Many!QualificationType" + }, + "generalisationOf": { + "$ref": "#/$defs/Many!QualificationType" + }, + "isPartialQualification": { + "$ref": "#/$defs/BooleanType" + }, + "eqfLevel": { + "$ref": "#/$defs/ConceptType" + }, + "nqfLevel": { + "$ref": "#/$defs/Many!ConceptType" + }, + "accreditation": { + "$ref": "#/$defs/Many!AccreditationType" + }, + "qualificationCode": { + "$ref": "#/$defs/Many!ConceptType" + }, + "status": { + "$ref": "#/$defs/StringType" + } + }, + "required": ["title"] + }, + "Many!LearningOutcomeType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningOutcomeType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/LearningOutcomeType" + } + } + ] + }, + "LearningOutcomeType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "LearningOutcome" + }, + "dcType": { + "$ref": "#/$defs/ConceptType" + }, + "identifier": { + "$ref": "#/$defs/IdentifierOrLegalIdentifierType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "reusabilityLevel": { + "$ref": "#/$defs/ConceptType" + }, + "relatedSkill": { + "$ref": "#/$defs/Many!ConceptType" + }, + "relatedESCOSkill": { + "$ref": "#/$defs/Many!ConceptType" + } + }, + "required": ["title"] + }, + "Many!ContactPointType": { + "anyOf": [ + { + "$ref": "#/$defs/ContactPointType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/ContactPointType" + } + } + ] + }, + "ContactPointType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "ContactPoint" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "address": { + "$ref": "#/$defs/Many!AddressType" + }, + "phone": { + "$ref": "#/$defs/Many!PhoneType" + }, + "emailAddress": { + "$ref": "#/$defs/Many!MailboxType" + }, + "contactForm": { + "$ref": "#/$defs/Many!WebResourceType" + } + }, + "required": [] + }, + "Many!NoteType": { + "anyOf": [ + { + "$ref": "#/$defs/NoteType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/NoteType" + } + } + ] + }, + "NoteType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Note" + }, + "noteLiteral": { + "$ref": "#/$defs/Many!LangStringType" + }, + "subject": { + "$ref": "#/$defs/ConceptType" + }, + "noteFormat": { + "$ref": "#/$defs/ConceptType" + } + }, + "required": ["noteLiteral"] + }, + "Many!AddressType": { + "anyOf": [ + { + "$ref": "#/$defs/AddressType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/AddressType" + } + } + ] + }, + "AddressType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Address" + }, + "identifier": { + "$ref": "#/$defs/IdentifierOrLegalIdentifierType" + }, + "fullAddress": { + "$ref": "#/$defs/NoteType" + }, + "countryCode": { + "$ref": "#/$defs/ConceptType" + } + }, + "required": ["countryCode"] + }, + "Many!PhoneType": { + "anyOf": [ + { + "$ref": "#/$defs/PhoneType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/PhoneType" + } + } + ] + }, + "PhoneType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Phone" + }, + "phoneNumber": { + "$ref": "#/$defs/StringType" + }, + "countryDialing": { + "$ref": "#/$defs/StringType" + }, + "areaDialing": { + "$ref": "#/$defs/StringType" + }, + "dialNumber": { + "$ref": "#/$defs/StringType" + } + }, + "required": [] + }, + "Many!MailboxType": { + "anyOf": [ + { + "$ref": "#/$defs/MailboxType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/MailboxType" + } + } + ] + }, + "MailboxType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/EmailType" + }, + "type": { + "const": "Mailbox" + } + }, + "required": [] + }, + "Many!WebResourceType": { + "anyOf": [ + { + "$ref": "#/$defs/WebResourceType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/WebResourceType" + } + } + ] + }, + "WebResourceType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "WebResource" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "language": { + "$ref": "#/$defs/ConceptType" + }, + "contentURL": { + "$ref": "#/$defs/URIType" + } + }, + "required": ["contentURL"] + }, + "Many!ConceptType": { + "anyOf": [ + { + "$ref": "#/$defs/ConceptType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/ConceptType" + } + } + ] + }, + "Single!ConceptType": { + "anyOf": [ + { + "$ref": "#/$defs/ConceptType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/ConceptType" + }, + "minItems": 1, + "maxItems": 1 + } + ] + }, + "ConceptType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Concept" + }, + "prefLabel": { + "$ref": "#/$defs/Many!LangStringType" + }, + "notation": { + "$ref": "#/$defs/LiteralType" + }, + "inScheme": { + "$ref": "#/$defs/ConceptSchemeType" + }, + "definition": { + "$ref": "#/$defs/Many!LangStringType" + } + }, + "required": [] + }, + "ConceptSchemeType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "ConceptScheme" + } + }, + "required": [] + }, + "Many!LocationType": { + "anyOf": [ + { + "$ref": "#/$defs/LocationType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/LocationType" + } + } + ] + }, + "LocationType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Location" + }, + "identifier": { + "$ref": "#/$defs/IdentifierOrLegalIdentifierType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "address": { + "$ref": "#/$defs/Many!AddressType" + }, + "geographicName": { + "$ref": "#/$defs/Many!AddressType" + }, + "spatialCode": { + "$ref": "#/$defs/Many!ConceptType" + }, + "geometry": { + "$ref": "#/$defs/Many!GeometryType" + } + }, + "required": ["address"] + }, + "Many!GeometryType": { + "anyOf": [ + { + "$ref": "#/$defs/GeometryType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/GeometryType" + } + } + ] + }, + "GeometryType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Geometry" + }, + "longitude": { + "$ref": "#/$defs/StringType" + }, + "latitude": { + "$ref": "#/$defs/StringType" + } + }, + "required": [] + }, + "Many!GroupType": { + "anyOf": [ + { + "$ref": "#/$defs/GroupType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/GroupType" + } + } + ] + }, + "GroupType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Group" + }, + "prefLabel": { + "$ref": "#/$defs/Many!LangStringType" + }, + "altLabel": { + "$ref": "#/$defs/Many!LangStringType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "location": { + "$ref": "#/$defs/Many!LocationType" + }, + "contactPoint": { + "$ref": "#/$defs/Many!ContactPointType" + }, + "member": { + "$ref": "#/$defs/Many!AgentOrPersonOrOrganisationType" + } + }, + "required": ["prefLabel"] + }, + "Many!AgentOrPersonOrOrganisationType": { + "anyOf": [ + { + "$ref": "#/$defs/AgentOrPersonOrOrganisationType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/AgentOrPersonOrOrganisationType" + } + } + ] + }, + "AgentOrPersonOrOrganisationType": { + "anyOf": [ + { + "$ref": "#/$defs/AgentType" + }, + { + "$ref": "#/$defs/PersonType" + }, + { + "$ref": "#/$defs/OrganisationType" + } + ] + }, + "LearningAchievementSpecificationOrSpecificationType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningAchievementSpecificationType" + }, + { + "$ref": "#/$defs/QualificationType" + } + ] + }, + "IdentifierOrLegalIdentifierType": { + "anyOf": [ + { + "$ref": "#/$defs/IdentifierType" + }, + { + "$ref": "#/$defs/LegalIdentifierType" + } + ] + }, + "Many!IdentifierType": { + "anyOf": [ + { + "$ref": "#/$defs/IdentifierType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/IdentifierType" + } + } + ] + }, + "IdentifierType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Identifier" + }, + "dcType": { + "$ref": "#/$defs/Many!ConceptType" + }, + "notation": { + "$ref": "#/$defs/LiteralType" + }, + "schemeAgency": { + "$ref": "#/$defs/LangStringType" + }, + "creator": { + "$ref": "#/$defs/IRIType" + }, + "dateIssued": { + "$ref": "#/$defs/DateTimeType" + }, + "schemeName": { + "$ref": "#/$defs/StringType" + }, + "schemeVersion": { + "$ref": "#/$defs/StringType" + }, + "schemeId": { + "$ref": "#/$defs/URIType" + } + }, + "required": ["notation"] + }, + "Many!LegalIdentifierType": { + "anyOf": [ + { + "$ref": "#/$defs/LegalIdentifierType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/LegalIdentifierType" + } + } + ] + }, + "LegalIdentifierType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "LegalIdentifier" + }, + "dcType": { + "$ref": "#/$defs/Many!ConceptType" + }, + "notation": { + "$ref": "#/$defs/LiteralType" + }, + "schemeAgency": { + "$ref": "#/$defs/LangStringType" + }, + "creator": { + "$ref": "#/$defs/IRIType" + }, + "dateIssued": { + "$ref": "#/$defs/DateTimeType" + }, + "schemeName": { + "$ref": "#/$defs/StringType" + }, + "schemeVersion": { + "$ref": "#/$defs/StringType" + }, + "schemeId": { + "$ref": "#/$defs/URIType" + }, + "spatial": { + "$ref": "#/$defs/ConceptType" + } + }, + "required": ["notation", "spatial"] + }, + "Many!CreditPointType": { + "anyOf": [ + { + "$ref": "#/$defs/CreditPointType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/CreditPointType" + } + } + ] + }, + "CreditPointType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "CreditPoint" + }, + "framework": { + "$ref": "#/$defs/ConceptType" + }, + "point": { + "$ref": "#/$defs/StringType" + } + }, + "required": ["framework", "point"] + }, + "AmountType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Amount" + }, + "unit": { + "$ref": "#/$defs/ConceptType" + }, + "value": { + "$ref": "#/$defs/DecimalType" + } + }, + "required": ["unit", "value"] + }, + "Many!LangStringType": { + "type": "object", + "propertyNames": { + "pattern": "^(aa|ab|ae|af|ak|am|an|ar|as|av|ay|az|ba|be|bg|bh|bi|bm|bn|bo|br|bs|ca|ce|ch|co|cr|cs|cu|cv|cy|da|de|dv|dz|ee|el|en|eo|es|et|eu|fa|ff|fi|fj|fo|fr|fy|ga|gd|gl|gn|gu|gv|ha|he|hi|ho|hr|ht|hu|hy|hz|ia|id|ie|ig|ii|ik|in|io|is|it|iu|iw|ja|ji|jv|jw|ka|kg|ki|kj|kk|kl|km|kn|ko|kr|ks|ku|kv|kw|ky|la|lb|lg|li|ln|lo|lt|lu|lv|mg|mh|mi|mk|ml|mn|mo|mr|ms|mt|my|na|nb|nd|ne|ng|nl|nn|no|nr|nv|ny|oc|oj|om|or|os|pa|pi|pl|ps|pt|qu|rm|rn|ro|ru|rw|sa|sc|sd|se|sg|sh|si|sk|sl|sm|sn|so|sq|sr|ss|st|su|sv|sw|ta|te|tg|th|ti|tk|tl|tn|to|tr|ts|tt|tw|ty|ug|uk|ur|uz|ve|vi|vo|wa|wo|xh|yi|yo|za|zh|zu)$" + }, + "minProperties": 1 + }, + "LangStringType": { + "allOf": [ + { + "$ref": "#/$defs/Many!LangStringType" + }, + { + "type": "object", + "maxProperties": 1 + } + ] + }, + "Many!LearningAchievementType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningAchievementType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/LearningAchievementType" + } + } + ] + }, + "LearningAchievementType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "LearningAchievement" + }, + "dcType": { + "$ref": "#/$defs/Many!ConceptType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "identifier": { + "$ref": "#/$defs/IdentifierOrLegalIdentifierType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "learningOpportunity": { + "$ref": "#/$defs/LearningOpportunityType" + }, + "creditReceived": { + "$ref": "#/$defs/Many!CreditPointType" + }, + "provenBy": { + "$ref": "#/$defs/Many!LearningAssessmentType" + }, + "influencedBy": { + "$ref": "#/$defs/Many!LearningActivityType" + }, + "awardedBy": { + "$ref": "#/$defs/AwardingProcessType" + }, + "entitlesTo": { + "$ref": "#/$defs/Many!LearningEntitlementType" + }, + "specifiedBy": { + "$ref": "#/$defs/LearningAchievementSpecificationOrQualificationType" + }, + "hasPart": { + "$ref": "#/$defs/Many!LearningAchievementType" + }, + "isPartOf": { + "$ref": "#/$defs/Many!LearningAchievementType" + } + }, + "required": ["title", "awardedBy"] + }, + "Many!LearningAchievementSpecificationType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningAchievementSpecificationType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/LearningAchievementSpecificationType" + } + } + ] + }, + "LearningAchievementSpecificationType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "LearningAchievementSpecification" + }, + "dcType": { + "$ref": "#/$defs/Many!ConceptType" + }, + "identifier": { + "$ref": "#/$defs/IdentifierOrLegalIdentifierType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "homepage": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "altLabel": { + "$ref": "#/$defs/Many!LangStringType" + }, + "category": { + "$ref": "#/$defs/Many!LangStringType" + }, + "dateModified": { + "$ref": "#/$defs/DateTimeType" + }, + "language": { + "$ref": "#/$defs/Many!ConceptType" + }, + "volumeOfLearning": { + "$ref": "#/$defs/DurationType" + }, + "mode": { + "$ref": "#/$defs/Many!ConceptType" + }, + "learningOutcomeSummary": { + "$ref": "#/$defs/NoteType" + }, + "thematicArea": { + "$ref": "#/$defs/Many!ConceptType" + }, + "educationSubject": { + "$ref": "#/$defs/Many!ConceptType" + }, + "creditPoint": { + "$ref": "#/$defs/Many!CreditPointType" + }, + "educationLevel": { + "$ref": "#/$defs/Many!ConceptType" + }, + "learningSetting": { + "$ref": "#/$defs/ConceptType" + }, + "maximumDuration": { + "$ref": "#/$defs/DurationType" + }, + "targetGroup": { + "$ref": "#/$defs/Many!ConceptType" + }, + "entryRequirement": { + "$ref": "#/$defs/NoteType" + }, + "learningOutcome": { + "$ref": "#/$defs/Many!LearningOutcomeType" + }, + "influencedBy": { + "$ref": "#/$defs/Many!LearningActivitySpecificationType" + }, + "provenBy": { + "$ref": "#/$defs/Many!LearningAssessmentSpecificationType" + }, + "entitlesTo": { + "$ref": "#/$defs/Many!LearningEntitlementSpecificationType" + }, + "awardingOpportunity": { + "$ref": "#/$defs/Many!AwardingOpportunityType" + }, + "hasPart": { + "$ref": "#/$defs/Many!LearningAchievementSpecificationOrQualificationType" + }, + "isPartOf": { + "$ref": "#/$defs/Many!LearningAchievementSpecificationOrQualificationType" + }, + "specialisationOf": { + "$ref": "#/$defs/Many!LearningAchievementSpecificationOrQualificationType" + }, + "generalisationOf": { + "$ref": "#/$defs/Many!LearningAchievementSpecificationOrQualificationType" + }, + "status": { + "$ref": "#/$defs/StringType" + } + }, + "required": ["title"] + }, + "Many!LearningActivityType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningActivityType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/LearningActivityType" + } + } + ] + }, + "LearningActivityType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "LearningActivity" + }, + "dcType": { + "$ref": "#/$defs/Many!ConceptType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "temporal": { + "$ref": "#/$defs/Many!PeriodOfTimeType" + }, + "location": { + "$ref": "#/$defs/Many!LocationType" + }, + "learningOpportunity": { + "$ref": "#/$defs/LearningOpportunityType" + }, + "workload": { + "$ref": "#/$defs/DurationType" + }, + "directedBy": { + "$ref": "#/$defs/Many!AgentOrPersonOrOrganisationType" + }, + "awardedBy": { + "$ref": "#/$defs/AwardingProcessType" + }, + "influences": { + "$ref": "#/$defs/Many!LearningAchievementType" + }, + "specifiedBy": { + "$ref": "#/$defs/LearningActivitySpecificationType" + }, + "hasPart": { + "$ref": "#/$defs/Many!LearningActivityType" + }, + "isPartOf": { + "$ref": "#/$defs/Many!LearningActivityType" + }, + "levelOfCompletion": { + "$ref": "#/$defs/PercentageIntegerType" + } + }, + "required": ["title", "awardedBy"] + }, + "Many!LearningActivitySpecificationType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningActivitySpecificationType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/LearningActivitySpecificationType" + } + } + ] + }, + "LearningActivitySpecificationType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "LearningActivitySpecification" + }, + "dcType": { + "$ref": "#/$defs/Many!ConceptType" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "homepage": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "altLabel": { + "$ref": "#/$defs/Many!LangStringType" + }, + "category": { + "$ref": "#/$defs/Many!LangStringType" + }, + "dateModified": { + "$ref": "#/$defs/DateTimeType" + }, + "language": { + "$ref": "#/$defs/Many!ConceptType" + }, + "volumeOfLearning": { + "$ref": "#/$defs/DurationType" + }, + "contactHour": { + "$ref": "#/$defs/Many!StringType" + }, + "mode": { + "$ref": "#/$defs/Many!ConceptType" + }, + "influences": { + "$ref": "#/$defs/Many!LearningAchievementSpecificationOrQualificationType" + }, + "hasPart": { + "$ref": "#/$defs/Many!LearningActivitySpecificationType" + }, + "isPartOf": { + "$ref": "#/$defs/Many!LearningActivitySpecificationType" + }, + "specialisationOf": { + "$ref": "#/$defs/Many!LearningActivitySpecificationType" + }, + "generalisationOf": { + "$ref": "#/$defs/Many!LearningActivitySpecificationType" + }, + "status": { + "$ref": "#/$defs/StringType" + } + }, + "required": ["title"] + }, + "Many!LearningAssessmentType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningAssessmentType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/LearningAssessmentType" + } + } + ] + }, + "LearningAssessmentType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "LearningAssessment" + }, + "dcType": { + "$ref": "#/$defs/Many!ConceptType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "dateIssued": { + "$ref": "#/$defs/DateTimeType" + }, + "location": { + "$ref": "#/$defs/LocationType" + }, + "grade": { + "$ref": "#/$defs/NoteType" + }, + "gradeStatus": { + "$ref": "#/$defs/ConceptType" + }, + "shortenedGrading": { + "$ref": "#/$defs/ShortenedGradingType" + }, + "resultDistribution": { + "$ref": "#/$defs/ResultDistributionType" + }, + "idVerification": { + "$ref": "#/$defs/ConceptType" + }, + "awardedBy": { + "$ref": "#/$defs/AwardingProcessType" + }, + "assessedBy": { + "$ref": "#/$defs/Many!AgentOrPersonOrOrganisationType" + }, + "proves": { + "$ref": "#/$defs/Many!LearningAchievementType" + }, + "hasPart": { + "$ref": "#/$defs/Many!LearningAssessmentType" + }, + "isPartOf": { + "$ref": "#/$defs/Many!LearningAssessmentType" + }, + "specifiedBy": { + "$ref": "#/$defs/Many!LearningAssessmentSpecificationType" + } + }, + "required": ["title", "grade", "awardedBy"] + }, + "Many!LearningAssessmentSpecificationType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningAssessmentSpecificationType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/LearningAssessmentSpecificationType" + } + } + ] + }, + "LearningAssessmentSpecificationType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "LearningAssessmentSpecification" + }, + "dcType": { + "$ref": "#/$defs/ConceptType" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "homepage": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "altLabel": { + "$ref": "#/$defs/Many!LangStringType" + }, + "category": { + "$ref": "#/$defs/Many!LangStringType" + }, + "dateModified": { + "$ref": "#/$defs/DateTimeType" + }, + "language": { + "$ref": "#/$defs/Many!ConceptType" + }, + "mode": { + "$ref": "#/$defs/Many!ConceptType" + }, + "gradingScheme": { + "$ref": "#/$defs/GradingSchemeType" + }, + "proves": { + "$ref": "#/$defs/Many!LearningAchievementSpecificationOrQualificationType" + }, + "hasPart": { + "$ref": "#/$defs/Many!LearningAssessmentSpecificationType" + }, + "isPartOf": { + "$ref": "#/$defs/Many!LearningAssessmentSpecificationType" + }, + "specialisationOf": { + "$ref": "#/$defs/Many!LearningAssessmentSpecificationType" + }, + "generalisationOf": { + "$ref": "#/$defs/Many!LearningAssessmentSpecificationType" + }, + "status": { + "$ref": "#/$defs/StringType" + } + }, + "required": ["title"] + }, + "Many!LearningEntitlementType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningEntitlementType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/LearningEntitlementType" + } + } + ] + }, + "LearningEntitlementType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "LearningEntitlement" + }, + "dcType": { + "$ref": "#/$defs/Many!ConceptType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "dateIssued": { + "$ref": "#/$defs/DateTimeType" + }, + "expiryDate": { + "$ref": "#/$defs/DateTimeType" + }, + "awardedBy": { + "$ref": "#/$defs/AwardingProcessType" + }, + "entitledBy": { + "$ref": "#/$defs/Many!LearningAchievementType" + }, + "hasPart": { + "$ref": "#/$defs/Many!LearningEntitlementType" + }, + "isPartOf": { + "$ref": "#/$defs/Many!LearningEntitlementType" + }, + "specifiedBy": { + "$ref": "#/$defs/Many!LearningEntitlementSpecificationType" + } + }, + "required": ["title", "awardedBy"] + }, + "Many!LearningEntitlementSpecificationType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningEntitlementSpecificationType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/LearningEntitlementSpecificationType" + } + } + ] + }, + "LearningEntitlementSpecificationType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "LearningEntitlementSpecification" + }, + "dcType": { + "$ref": "#/$defs/Single!ConceptType" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "homepage": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "altLabel": { + "$ref": "#/$defs/Many!LangStringType" + }, + "category": { + "$ref": "#/$defs/Many!LangStringType" + }, + "dateModified": { + "$ref": "#/$defs/DateTimeType" + }, + "entitlementStatus": { + "$ref": "#/$defs/ConceptType" + }, + "limitOrganisation": { + "$ref": "#/$defs/Many!OrganisationType" + }, + "limitJurisdiction": { + "$ref": "#/$defs/Many!ConceptType" + }, + "limitOccupation": { + "$ref": "#/$defs/Many!ConceptType" + }, + "limitNationalOccupation": { + "$ref": "#/$defs/Many!ConceptType" + }, + "entitledBy": { + "$ref": "#/$defs/Many!LearningAchievementSpecificationOrQualificationType" + }, + "hasPart": { + "$ref": "#/$defs/Many!LearningEntitlementSpecificationType" + }, + "isPartOf": { + "$ref": "#/$defs/Many!LearningEntitlementSpecificationType" + }, + "specialisationOf": { + "$ref": "#/$defs/Many!LearningEntitlementSpecificationType" + }, + "generalisationOf": { + "$ref": "#/$defs/Many!LearningEntitlementSpecificationType" + }, + "status": { + "$ref": "#/$defs/StringType" + } + }, + "required": ["title", "entitlementStatus", "dcType"] + }, + "Many!LearningOpportunityType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningOpportunityType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/LearningOpportunityType" + } + } + ] + }, + "LearningOpportunityType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "LearningOpportunity" + }, + "dcType": { + "$ref": "#/$defs/Many!ConceptType" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "homepage": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "temporal": { + "$ref": "#/$defs/PeriodOfTimeType" + }, + "duration": { + "$ref": "#/$defs/DurationType" + }, + "mode": { + "$ref": "#/$defs/Many!ConceptType" + }, + "learningSchedule": { + "$ref": "#/$defs/ConceptType" + }, + "scheduleInformation": { + "$ref": "#/$defs/NoteType" + }, + "admissionProcedure": { + "$ref": "#/$defs/NoteType" + }, + "priceDetail": { + "$ref": "#/$defs/Many!PriceDetailType" + }, + "providedBy": { + "$ref": "#/$defs/OrganisationType" + }, + "grant": { + "$ref": "#/$defs/Many!GrantType" + }, + "location": { + "$ref": "#/$defs/Many!LocationType" + }, + "learningAchievementSpecification": { + "$ref": "#/$defs/LearningAchievementSpecificationOrQualificationType" + }, + "learningActivitySpecification": { + "$ref": "#/$defs/LearningActivitySpecificationType" + }, + "hasPart": { + "$ref": "#/$defs/Many!LearningOpportunityType" + }, + "isPartOf": { + "$ref": "#/$defs/Many!LearningOpportunityType" + }, + "bannerImage": { + "$ref": "#/$defs/MediaObjectType" + }, + "applicationDeadline": { + "$ref": "#/$defs/Many!DateTimeType" + }, + "defaultLanguage": { + "$ref": "#/$defs/ConceptType" + }, + "descriptionHtml": { + "$ref": "#/$defs/Many!HTMLType" + }, + "dateModified": { + "$ref": "#/$defs/DateTimeType" + }, + "status": { + "$ref": "#/$defs/StringType" + } + }, + "required": ["title"] + }, + "Many!PriceDetailType": { + "anyOf": [ + { + "$ref": "#/$defs/PriceDetailType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/PriceDetailType" + } + } + ] + }, + "PriceDetailType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "PriceDetail" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "prefLabel": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "amount": { + "$ref": "#/$defs/AmountType" + } + }, + "required": [] + }, + "Many!ResultCategoryType": { + "anyOf": [ + { + "$ref": "#/$defs/ResultCategoryType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/ResultCategoryType" + } + } + ] + }, + "ResultCategoryType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "ResultCategory" + }, + "label": { + "$ref": "#/$defs/StringType" + }, + "score": { + "$ref": "#/$defs/StringType" + }, + "maximumScore": { + "$ref": "#/$defs/StringType" + }, + "minimumScore": { + "$ref": "#/$defs/StringType" + }, + "count": { + "$ref": "#/$defs/PositiveIntegerType" + } + }, + "required": ["label", "count"] + }, + "Many!ResultDistributionType": { + "anyOf": [ + { + "$ref": "#/$defs/ResultDistributionType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/ResultDistributionType" + } + } + ] + }, + "ResultDistributionType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "ResultDistribution" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "resultCategory": { + "$ref": "#/$defs/Many!ResultCategoryType" + } + }, + "required": [] + }, + "Many!ShortenedGradingType": { + "anyOf": [ + { + "$ref": "#/$defs/ShortenedGradingType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/ShortenedGradingType" + } + } + ] + }, + "ShortenedGradingType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "ShortenedGrading" + }, + "percentageLower": { + "$ref": "#/$defs/IntegerType" + }, + "percentageEqual": { + "$ref": "#/$defs/IntegerType" + }, + "percentageHigher": { + "$ref": "#/$defs/IntegerType" + } + }, + "required": ["percentageLower", "percentageEqual", "percentageHigher"] + }, + "Many!VerificationCheckType": { + "anyOf": [ + { + "$ref": "#/$defs/VerificationCheckType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/VerificationCheckType" + } + } + ] + }, + "VerificationCheckType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "VerificationCheck" + }, + "dcType": { + "$ref": "#/$defs/ConceptType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "verificationStatus": { + "$ref": "#/$defs/ConceptType" + }, + "elmSubject": { + "$ref": "#/$defs/EuropeanDigitalCredentialType" + } + }, + "required": ["verificationStatus", "subject", "dcType"] + }, + "Many!EvidenceType": { + "anyOf": [ + { + "$ref": "#/$defs/EvidenceType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/EvidenceType" + } + } + ] + }, + "EvidenceType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Evidence" + }, + "evidenceStatement": { + "$ref": "#/$defs/StringType" + }, + "evidenceTarget": { + "$ref": "#/$defs/AgentOrPersonOrOrganisationType" + }, + "embeddedEvidence": { + "$ref": "#/$defs/Many!MediaObjectType" + }, + "accreditation": { + "$ref": "#/$defs/AccreditationType" + }, + "dcType": { + "$ref": "#/$defs/ConceptType" + } + }, + "required": [] + }, + "Many!TermsOfUseType": { + "anyOf": [ + { + "$ref": "#/$defs/TermsOfUseType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/TermsOfUseType" + } + } + ] + }, + "TermsOfUseType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "TermsOfUse" + } + }, + "required": [] + }, + "Many!ProofType": { + "anyOf": [ + { + "$ref": "#/$defs/ProofType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/ProofType" + } + } + ] + }, + "ProofType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Proof" + } + }, + "required": [] + }, + "Many!CredentialStatusType": { + "anyOf": [ + { + "$ref": "#/$defs/CredentialStatusType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/CredentialStatusType" + } + } + ] + }, + "CredentialStatusType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "type": "string", + "enum": ["StatusList2021Entry"] + } + }, + "required": [] + }, + "Many!CredentialSchemaType": { + "anyOf": [ + { + "$ref": "#/$defs/CredentialSchemaType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/CredentialSchemaType" + } + } + ] + }, + "CredentialSchemaType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "type": "string", + "enum": ["ShaclValidator2017", "JsonSchema"] + } + }, + "required": [] + }, + "Many!AmountType": { + "anyOf": [ + { + "$ref": "#/$defs/AmountType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/AmountType" + } + } + ] + }, + "Many!AwardingProcessType": { + "anyOf": [ + { + "$ref": "#/$defs/AwardingProcessType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/AwardingProcessType" + } + } + ] + }, + "AwardingProcessType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "AwardingProcess" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "location": { + "$ref": "#/$defs/LocationType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "used": { + "$ref": "#/$defs/Many!LearningAssessmentType" + }, + "awards": { + "$ref": "#/$defs/Many!ClaimNodeType" + }, + "awardingBody": { + "$ref": "#/$defs/Many!AgentOrPersonOrOrganisationType" + }, + "awardingDate": { + "$ref": "#/$defs/DateTimeType" + }, + "educationalSystemNote": { + "$ref": "#/$defs/ConceptType" + } + }, + "required": ["awardingBody"] + }, + "Many!DisplayParameterType": { + "anyOf": [ + { + "$ref": "#/$defs/DisplayParameterType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/DisplayParameterType" + } + } + ] + }, + "DisplayParameterType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "DisplayParameter" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "language": { + "$ref": "#/$defs/Many!ConceptType" + }, + "primaryLanguage": { + "$ref": "#/$defs/ConceptType" + }, + "summaryDisplay": { + "$ref": "#/$defs/StringType" + }, + "individualDisplay": { + "$ref": "#/$defs/Many!IndividualDisplayType" + } + }, + "required": ["title", "language", "primaryLanguage", "individualDisplay"] + }, + "Many!IndividualDisplayType": { + "anyOf": [ + { + "$ref": "#/$defs/IndividualDisplayType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/IndividualDisplayType" + } + } + ] + }, + "IndividualDisplayType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "IndividualDisplay" + }, + "language": { + "$ref": "#/$defs/ConceptType" + }, + "displayDetail": { + "$ref": "#/$defs/Many!DisplayDetailType" + } + }, + "required": ["language", "displayDetail"] + }, + "Many!DisplayDetailType": { + "anyOf": [ + { + "$ref": "#/$defs/DisplayDetailType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/DisplayDetailType" + } + } + ] + }, + "DisplayDetailType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "DisplayDetail" + }, + "image": { + "$ref": "#/$defs/MediaObjectType" + }, + "page": { + "$ref": "#/$defs/PositiveIntegerType" + } + }, + "required": ["image", "page"] + }, + "Many!EuropeanDigitalPresentationType": { + "anyOf": [ + { + "$ref": "#/$defs/EuropeanDigitalPresentationType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/EuropeanDigitalPresentationType" + } + } + ] + }, + "EuropeanDigitalPresentationType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "EuropeanDigitalPresentation" + }, + "verifiableCredential": { + "$ref": "#/$defs/Many!EuropeanDigitalCredentialType" + }, + "verificationCheck": { + "$ref": "#/$defs/Many!VerificationCheckType" + }, + "holder": { + "$ref": "#/$defs/Many!AgentOrPersonOrOrganisationType" + }, + "proof": { + "$ref": "#/$defs/Many!ProofType" + } + }, + "required": [] + }, + "Many!GradingSchemeType": { + "anyOf": [ + { + "$ref": "#/$defs/GradingSchemeType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/GradingSchemeType" + } + } + ] + }, + "GradingSchemeType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "GradingScheme" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + } + }, + "required": ["title"] + }, + "Many!GrantType": { + "anyOf": [ + { + "$ref": "#/$defs/GrantType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/GrantType" + } + } + ] + }, + "GrantType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "Grant" + }, + "dcType": { + "$ref": "#/$defs/ConceptType" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "contentURL": { + "$ref": "#/$defs/URIType" + } + }, + "required": ["title"] + }, + "Many!ClaimTypeNodeType": { + "anyOf": [ + { + "$ref": "#/$defs/ClaimTypeNodeType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/ClaimTypeNodeType" + } + } + ] + }, + "ClaimTypeNodeType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "ClaimTypeNode" + }, + "title": { + "$ref": "#/$defs/Many!LangStringType" + }, + "description": { + "$ref": "#/$defs/Many!LangStringType" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "additionalNote": { + "$ref": "#/$defs/Many!NoteType" + }, + "supplementaryDocument": { + "$ref": "#/$defs/Many!WebResourceType" + }, + "awardedBy": { + "$ref": "#/$defs/AwardingProcessType" + } + }, + "required": ["title", "awardedBy"] + }, + "EuropeanDigitalCredentialType": { + "type": "object", + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "VerifiableCredential", + "VerifiableAttestation", + "EuropeanDigitalCredential" + ] + }, + "minItems": 3, + "uniqueItems": true + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "credentialProfiles": { + "$ref": "#/$defs/Many!ConceptType" + }, + "attachment": { + "$ref": "#/$defs/Many!MediaObjectType" + }, + "displayParameter": { + "$ref": "#/$defs/DisplayParameterType" + }, + "issuer": { + "anyOf": [ + { + "$ref": "#/$defs/AgentOrPersonOrOrganisationType" + }, + { + "$ref": "#/$defs/GenericIdType" + } + ] + }, + "credentialSubject": { + "$ref": "#/$defs/AgentOrPersonOrOrganisationType" + }, + "issuanceDate": { + "$ref": "#/$defs/DateTimeType" + }, + "issued": { + "$ref": "#/$defs/DateTimeType" + }, + "validFrom": { + "$ref": "#/$defs/DateTimeType" + }, + "expirationDate": { + "$ref": "#/$defs/Many!DateTimeType" + }, + "validUntil": { + "$ref": "#/$defs/DateTimeType" + }, + "proof": { + "$ref": "#/$defs/Many!ProofType" + }, + "evidence": { + "$ref": "#/$defs/Many!EvidenceType" + }, + "termsOfUse": { + "$ref": "#/$defs/Many!TermsOfUseType" + }, + "credentialSchema": { + "$ref": "#/$defs/Many!CredentialSchemaType" + }, + "credentialStatus": { + "$ref": "#/$defs/Many!CredentialStatusType" + }, + "holder": { + "$ref": "#/$defs/Many!AgentOrPersonOrOrganisationType" + } + }, + "required": [ + "credentialProfiles", + "displayParameter", + "issuer", + "credentialSubject", + "issued", + "validFrom", + "credentialSchema" + ] + }, + "Many!IdentifierOrLegalIdentifierType": { + "anyOf": [ + { + "$ref": "#/$defs/IdentifierOrLegalIdentifierType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/IdentifierOrLegalIdentifierType" + } + } + ] + }, + "Many!MediaObjectType": { + "anyOf": [ + { + "$ref": "#/$defs/MediaObjectType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/MediaObjectType" + } + } + ] + }, + "Many!DateTimeType": { + "anyOf": [ + { + "$ref": "#/$defs/DateTimeType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/DateTimeType" + } + } + ] + }, + "Many!LearningAchievementSpecificationOrQualificationType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningAchievementSpecificationOrQualificationType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/LearningAchievementSpecificationOrQualificationType" + } + } + ] + }, + "LearningAchievementSpecificationOrQualificationType": { + "anyOf": [ + { + "$ref": "#/$defs/LearningAchievementSpecificationType" + }, + { + "$ref": "#/$defs/QualificationType" + } + ] + }, + "Many!AwardingOpportunityType": { + "anyOf": [ + { + "$ref": "#/$defs/AwardingOpportunityType" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/AwardingOpportunityType" + } + } + ] + }, + "AwardingOpportunityType": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "$ref": "#/$defs/GenericIdType" + }, + "type": { + "const": "AwardingOpportunity" + }, + "identifier": { + "$ref": "#/$defs/Many!IdentifierOrLegalIdentifierType" + }, + "location": { + "$ref": "#/$defs/LocationType" + }, + "temporal": { + "$ref": "#/$defs/PeriodOfTimeType" + }, + "awardingBody": { + "$ref": "#/$defs/Many!AgentOrPersonOrOrganisationType" + }, + "learningAchievementSpecification": { + "$ref": "#/$defs/LearningAchievementSpecificationOrQualificationType" + } + }, + "required": ["awardingBody"] + } + } + } + \ No newline at end of file diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index c1b3681..8c1f647 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -201,6 +201,18 @@ "path": "$.credentialSubject.hasClaim[0].awardedBy.awardingBody[0].legalName.en[0]" } }, + { + "type_": "addressToLocation", + "source": { + "format": "OBv3", + "path": "$.issuer.address" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].awardedBy.awardingBody[0].location" + } + }, + { "type_": "copy", "source": { diff --git a/src/backend/base64_encode.rs b/src/backend/base64_encode.rs index 47d0fee..1d49608 100644 --- a/src/backend/base64_encode.rs +++ b/src/backend/base64_encode.rs @@ -6,6 +6,21 @@ use std::io::Read; use std::path::Path; use ureq::get; +/// Decode json input from Base64, and returns the byte array. +/// +/// # Arguments +/// - `string`: The string of the json to decode. +/// +/// # Returns +/// - `Ok(Byte)`: The Base64-encoded string of the image if successful. +/// - `Err(Box)`: An error if the fetch or encoding fails. +pub fn decode_json(json: &str) -> Result, Box> { + // Encode the image bytes as a Base64 string + let conv_byte = Base64Engine.decode(json)?; + + Ok(conv_byte) +} + /// Fetches an image from the given URL, encodes it in Base64, and returns the encoded string. /// /// # Arguments @@ -211,7 +226,6 @@ pub fn image_to_individual_display(image_value: Value) -> Value { "en": ["base64"] } }, - "page": 1, "contentType": { "id": "http://publications.europa.eu/resource/authority/file-type/JPEG", "type": "Concept", @@ -224,7 +238,8 @@ pub fn image_to_individual_display(image_value: Value) -> Value { }, "notation": "file-type" } - } + }, + "page": 1 } ] } diff --git a/src/backend/elm_mapping_helper.rs b/src/backend/elm_mapping_helper.rs index 9d9aebe..0cab243 100644 --- a/src/backend/elm_mapping_helper.rs +++ b/src/backend/elm_mapping_helper.rs @@ -1,7 +1,6 @@ use codes_iso_3166::part_1::CountryCode; -use std::str::FromStr; use serde_json::Value; - +use std::str::FromStr; /// Creates country code based on input type in string found in addressCountryCode /// @@ -11,7 +10,6 @@ use serde_json::Value; /// # Returns /// - Value: The content value Object in ELM format if successful. pub fn address_to_location(address_value: Value) -> Value { - //inspect the address object (address as used in issuer for now) and re write it so it can be reused in ELM //we need to achieve the following structure into the indivudualDisplay array: let json_data = r#" @@ -58,13 +56,12 @@ pub fn address_to_location(address_value: Value) -> Value { //println!("The addressCountryCode field does not exist."); } - parsed_json["address"]["countryCode"]["id"] = Value::String(format!("http://publications.europa.eu/resource/authority/language/{}",country.alpha_3_code().unwrap())); + parsed_json["address"]["countryCode"]["id"] = Value::String(format!( + "http://publications.europa.eu/resource/authority/language/{}", + country.alpha_3_code().unwrap() + )); parsed_json["address"]["countryCode"]["prefLabel"]["en"] = Value::String(country.full_name().unwrap().to_string()); - //println!("{:#?}", parsed_json); parsed_json - - } - diff --git a/src/backend/init_conversion.rs b/src/backend/init_conversion.rs index 756acaf..c4fb51b 100644 --- a/src/backend/init_conversion.rs +++ b/src/backend/init_conversion.rs @@ -94,7 +94,7 @@ fn enter_fixed_context_values(state: &mut AppState) { output_elm.insert( "@context".to_string(), Value::Array(vec![ - json!("https://www.w3.org/ns/credentials/v2"), + json!("https://www.w3.org/2018/credentials/v1"), json!("http://data.europa.eu/snb/model/context/edc-ap"), ]), ); diff --git a/src/backend/routes/api.rs b/src/backend/routes/api.rs new file mode 100644 index 0000000..8c43978 --- /dev/null +++ b/src/backend/routes/api.rs @@ -0,0 +1,148 @@ +use axum::{ + // routing::post, + // Router, + http::{header, HeaderMap, HeaderValue, StatusCode}, + response::{IntoResponse, Response}, + Json, +}; +//use config::Value; +use serde_json::Value; + +use crate::backend::base64_encode::decode_json; +use crate::backend::headless_cli::load_files_apply_transformations; +use crate::state::{AppState, Mapping}; +use std::{fs::File, io::Write, path::Path}; +use tokio::fs; + +pub async fn api(Json(input_json): Json) -> Result { + //test the input types for this API + // with JSON body: { + // "From": {"Name": "OB", "Version": "3.0"}, + // "To": {"Name": "elm", "Version": "3.2"}, + // "Parameters": { "PreferredLanguages": ["en", "sv"]}, + // "Content": "Base 64 encoded content in From format" + // } + + // Handle the file upload + let mut _input_file_path = String::new(); + let mut _mapping_file_name = String::new(); + let mut _mapping_type = Mapping::default(); + + // Create directories for uploads and outputs if they don't exist + let upload_dir = "uploads"; + let output_dir = "outputs"; + let file_name = "export_file"; + fs::create_dir_all(upload_dir).await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to create upload directory".to_string(), + ) + })?; + fs::create_dir_all(output_dir).await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to create output directory".to_string(), + ) + })?; + + match input_json + .get("From") + .and_then(|v| v.get("Name")) + .and_then(|v| v.as_str()) + { + Some("OB") => { + _mapping_file_name = "json/mapping/custom_mapping_OBv3_ELM_latest.json".to_string(); + _mapping_type = Mapping::OBv3ToELM; + } + Some("ELM") => { + _mapping_file_name = "json/mapping/custom_mapping_ELM_OBv3_latest.json".to_string(); + _mapping_type = Mapping::ELMToOBv3; + } + Some(value) => return Err((StatusCode::BAD_REQUEST, format!("Invalid translation value: {}", value))), + None => { + return Err(( + StatusCode::BAD_REQUEST, + "Invalid translation value: no key found".to_string(), + )) + } + } + + match input_json.get("Content").and_then(|v| v.as_str()) { + Some(value) => { + _input_file_path = format!("{}/{}", upload_dir, file_name); + let mut file = File::create(&_input_file_path) + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to create file".to_string()))?; + let data = decode_json(value).map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to read file data".to_string(), + ) + })?; + file.write_all(&data) + .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to write to file".to_string()))?; + } + None => return Err((StatusCode::BAD_REQUEST, "Invalid data value: no key found".to_string())), + } + + // Define the output file path + let output_file_name = format!( + "translated_{}", + Path::new(&_input_file_path).file_name().unwrap().to_str().unwrap() + ); + let output_file_path = format!("{}/{}", output_dir, output_file_name); + + // start mapping based on the input form the API + // 1 create a state needed for the mapping tool + // 2 load all hte state elements needed for mapping + + let mut state = AppState::default(); + state.input_path = _input_file_path; + state.output_path = output_file_path.clone(); + state.mapping_path = _mapping_file_name; + state.mapping = _mapping_type; + + load_files_apply_transformations(&mut state); + + // Return the translated file as a response + // 1 load file from fs into mem + // 2 remove file from fs + // 3 push mem to http output + + let output_file = fs::read(&output_file_path).await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to read output file".to_string(), + ) + })?; + fs::remove_file(state.input_path).await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to remove output file".to_string(), + ) + })?; + fs::remove_file(state.output_path).await.map_err(|_| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to remove output file".to_string(), + ) + })?; + + // Set the headers, including content disposition for download + let mut headers = HeaderMap::new(); + // For better integration into EDCI change the output_file_name from *.json to *.jsonld + let mut long_output_file_name = output_file_name; + long_output_file_name.push_str("ld"); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_static("application/octet-stream"), + ); + headers.insert( + header::CONTENT_DISPOSITION, + HeaderValue::from_str(&format!("attachment; filename=\"{}\"", long_output_file_name)).unwrap(), + ); + + // Return the file content along with the appropriate headers + Ok((headers, output_file).into_response()) + + // Ok(output_file.into_response()) +} diff --git a/src/backend/routes/mod.rs b/src/backend/routes/mod.rs index 243f811..fa62726 100644 --- a/src/backend/routes/mod.rs +++ b/src/backend/routes/mod.rs @@ -1,8 +1,10 @@ +pub mod api; pub mod root; pub mod translate_file; use axum::{extract::DefaultBodyLimit, routing::get, routing::post, Router}; //use save_file::save_file; +use api::api; use root::root; use translate_file::translate_file; @@ -12,6 +14,7 @@ pub fn create_router() -> Router { "/translate_file", post(translate_file).route_layer(DefaultBodyLimit::max(135476000)), ) + .route("/api", post(api).route_layer(DefaultBodyLimit::max(135476000))) .route("/", get(root)) // .layer(tower_http::trace::TraceLayer::new_for_http()) } diff --git a/test/encoded_test.json b/test/encoded_test.json new file mode 100644 index 0000000..1a4abc8 --- /dev/null +++ b/test/encoded_test.json @@ -0,0 +1,6 @@ +{ + "From": {"Name": "OB", "Version": "3.0"}, + "To": {"Name": "elm", "Version": "3.2"}, + "Parameters": { "PreferredLanguages": ["en", "sv"]}, + "Content" : "ewogICAgIkBjb250ZXh0IjogWwogICAgICAiaHR0cHM6Ly93d3cudzMub3JnL25zL2NyZWRlbnRpYWxzL3YyIiwKICAgICAgImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9jb250ZXh0LTMuMC4zLmpzb24iLAogICAgICAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2V4dGVuc2lvbnMuanNvbiIKICAgIF0sCiAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2NyZWRlbnRpYWxzLzM3MzIiLAogICAgInR5cGUiOiBbCiAgICAgICJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsCiAgICAgICJPcGVuQmFkZ2VDcmVkZW50aWFsIgogICAgXSwKICAgICJuYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUgZm9yIEV4YW1wbGUgU3R1ZGVudCIsCiAgICAiZGVzY3JpcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IERlZ3JlZSBEZXNjcmlwdGlvbiIsCiAgICAiaW1hZ2UiOiB7CiAgICAgICJpZCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vaGFtcnQvY3JlZGVudGlhbC1jb252ZXJ0ZXIvcmVmcy9oZWFkcy9pbWFnZS90ZXN0L2VkdWJhZGdlc18xMDB4MTAwLnBuZyIsCiAgICAgICJ0eXBlIjogIkltYWdlIiwKICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IERlZ3JlZSBmb3IgRXhhbXBsZSBTdHVkZW50IgogICAgfSwKICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgImlkIjogImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsCiAgICAgICJ0eXBlIjogWwogICAgICAgICJBY2hpZXZlbWVudFN1YmplY3QiCiAgICAgIF0sCiAgICAgICJhY3Rpdml0eUVuZERhdGUiOiAiMjAxMC0wMS0wMlQwMDowMDowMFoiLAogICAgICAiYWN0aXZpdHlTdGFydERhdGUiOiAiMjAxMC0wMS0wMVQwMDowMDowMFoiLAogICAgICAiY3JlZGl0c0Vhcm5lZCI6IDQyLAogICAgICAibGljZW5zZU51bWJlciI6ICJBLTkzMjAwNDEiLAogICAgICAicm9sZSI6ICJNYWpvciBEb21vIiwKICAgICAgInNvdXJjZSI6IHsKICAgICAgICAiaWQiOiAiaHR0cHM6Ly9zY2hvb2wuZWR1L2lzc3VlcnMvMjAxMjM0IiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJQcm9maWxlIgogICAgICAgIF0sCiAgICAgICAgIm5hbWUiOiAiMUVkVGVjaCBDb2xsZWdlIG9mIEFydHMiCiAgICAgIH0sCiAgICAgICJ0ZXJtIjogIkZhbGwiLAogICAgICAiaWRlbnRpZmllciI6IFsKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6ICJJZGVudGl0eU9iamVjdCIsCiAgICAgICAgICAiaWRlbnRpdHlIYXNoIjogInN0dWRlbnRAMWVkdGVjaC5lZHUiLAogICAgICAgICAgImlkZW50aXR5VHlwZSI6ICJlbWFpbEFkZHJlc3MiLAogICAgICAgICAgImhhc2hlZCI6IGZhbHNlLAogICAgICAgICAgInNhbHQiOiAibm90LXVzZWQiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6ICJJZGVudGl0eU9iamVjdCIsCiAgICAgICAgICAiaWRlbnRpdHlIYXNoIjogInNvbWVib2R5QGdtYWlsLmNvbSIsCiAgICAgICAgICAiaWRlbnRpdHlUeXBlIjogImVtYWlsQWRkcmVzcyIsCiAgICAgICAgICAiaGFzaGVkIjogZmFsc2UsCiAgICAgICAgICAic2FsdCI6ICJub3QtdXNlZCIKICAgICAgICB9CiAgICAgIF0sCiAgICAgICJhY2hpZXZlbWVudCI6IHsKICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9hY2hpZXZlbWVudHMvZGVncmVlIiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJBY2hpZXZlbWVudCIKICAgICAgICBdLAogICAgICAgICJhbGlnbm1lbnQiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJBbGlnbm1lbnQiCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogImRlZ3JlZSIsCiAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgRGVncmVlIHByb2dyYW1zLiIsCiAgICAgICAgICAgICJ0YXJnZXROYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUiLAogICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICJ0YXJnZXRUeXBlIjogIkNGSXRlbSIsCiAgICAgICAgICAgICJ0YXJnZXRVcmwiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9jYXRhbG9nL2RlZ3JlZSIKICAgICAgICAgIH0sCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJBbGlnbm1lbnQiCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogImRlZ3JlZSIsCiAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgRGVncmVlIHByb2dyYW1zLiIsCiAgICAgICAgICAgICJ0YXJnZXROYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUiLAogICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICJ0YXJnZXRUeXBlIjogIkNUREwiLAogICAgICAgICAgICAidGFyZ2V0VXJsIjogImh0dHBzOi8vY3JlZGVudGlhbGVuZ2luZXJlZ2lzdHJ5Lm9yZy9yZXNvdXJjZXMvY2UtOThjYjAyN2ItOTVlZi00NDk0LTkwOGQtNmY3NzkwZWM2YjZiIgogICAgICAgICAgfQogICAgICAgIF0sCiAgICAgICAgImFjaGlldmVtZW50VHlwZSI6ICJEZWdyZWUiLAogICAgICAgICJjcmVhdG9yIjogewogICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgXSwKICAgICAgICAgICJuYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSIsCiAgICAgICAgICAidXJsIjogImh0dHBzOi8vMWVkdGVjaC5lZHUiLAogICAgICAgICAgInBob25lIjogIjEtMjIyLTMzMy00NDQ0IiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgcHJvdmlkZXMgb25saW5lIGRlZ3JlZSBwcm9ncmFtcy4iLAogICAgICAgICAgImVuZG9yc2VtZW50IjogWwogICAgICAgICAgICB7CiAgICAgICAgICAgICAgIkBjb250ZXh0IjogWwogICAgICAgICAgICAgICAgImh0dHBzOi8vd3d3LnczLm9yZy9ucy9jcmVkZW50aWFscy92MiIsCiAgICAgICAgICAgICAgICAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjMuanNvbiIKICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvZW5kb3JzZW1lbnRjcmVkZW50aWFsLzM3MzIiLAogICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgIlZlcmlmaWFibGVDcmVkZW50aWFsIiwKICAgICAgICAgICAgICAgICJFbmRvcnNlbWVudENyZWRlbnRpYWwiCiAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAibmFtZSI6ICJTREUgZW5kb3JzZW1lbnQiLAogICAgICAgICAgICAgICJpc3N1ZXIiOiB7CiAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSIsCiAgICAgICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAgICAgIlByb2ZpbGUiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgIm5hbWUiOiAiRXhhbXBsZSBBY2NyZWRpdGluZyBBZ2VuY3kiCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAidmFsaWRGcm9tIjogIjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwKICAgICAgICAgICAgICAidmFsaWRVbnRpbCI6ICIyMDIwLTAxLTAxVDAwOjAwOjAwWiIsCiAgICAgICAgICAgICAgImNyZWRlbnRpYWxTdWJqZWN0IjogewogICAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgICAgICJFbmRvcnNlbWVudFN1YmplY3QiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgImVuZG9yc2VtZW50Q29tbWVudCI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgaXMgaW4gZ29vZCBzdGFuZGluZyIKICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICJjcmVkZW50aWFsU2NoZW1hIjogWwogICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL3NjaGVtYS9qc29uL29iX3YzcDBfZW5kb3JzZW1lbnRjcmVkZW50aWFsX3NjaGVtYS5qc29uIiwKICAgICAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaEpzb25TY2hlbWFWYWxpZGF0b3IyMDE5IgogICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vYWNjcmVkaXRlci5lZHUvc2NoZW1hL2VuZG9yc2VtZW50Y3JlZGVudGlhbC5qc29uIiwKICAgICAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaEpzb25TY2hlbWFWYWxpZGF0b3IyMDE5IgogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgImNyZWRlbnRpYWxTdGF0dXMiOiB7CiAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9jcmVkZW50aWFscy8zNzMyL3Jldm9jYXRpb25zIiwKICAgICAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hSZXZvY2F0aW9uTGlzdCIKICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICJyZWZyZXNoU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMiIsCiAgICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoQ3JlZGVudGlhbFJlZnJlc2giCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAicHJvb2YiOiBbCiAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICJ0eXBlIjogIkRhdGFJbnRlZ3JpdHlQcm9vZiIsCiAgICAgICAgICAgICAgICAgICJjcnlwdG9zdWl0ZSI6ICJlZGRzYS1yZGYtMjAyMiIsCiAgICAgICAgICAgICAgICAgICJjcmVhdGVkIjogIjIwMjItMDUtMjZUMTg6MTc6MDhaIiwKICAgICAgICAgICAgICAgICAgInZlcmlmaWNhdGlvbk1ldGhvZCI6ICJodHRwczovL2FjY3JlZGl0ZXIuZWR1L2lzc3VlcnMvNTY1MDQ5I3p2UGtRaVVGZkpyZ25DUmh5UGtUU2tnckdYYm5MUjE1cEhINUhaVllOZE00VENBd1FIcUc3Zk1lTVBMdFlOUm5FZ29WMWFKZFI1RTYxZVd1NXNXUllndEEiLAogICAgICAgICAgICAgICAgICAicHJvb2ZQdXJwb3NlIjogImFzc2VydGlvbk1ldGhvZCIsCiAgICAgICAgICAgICAgICAgICJwcm9vZlZhbHVlIjogInp2UGtRaVVGZkpyZ25DUmh5UGtUU2tnckdYYm5MUjE1cEhINUhaVllOZE00VENBd1FIcUc3Zk1lTVBMdFlOUm5FZ29WMWFKZFI1RTYxZVd1NXNXUllndEEiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgXQogICAgICAgICAgICB9LAogICAgICAgICAgICB7CiAgICAgICAgICAgICAgIkBjb250ZXh0IjogWwogICAgICAgICAgICAgICAgImh0dHBzOi8vd3d3LnczLm9yZy9ucy9jcmVkZW50aWFscy92MiIsCiAgICAgICAgICAgICAgICAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjMuanNvbiIKICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvZW5kb3JzZW1lbnRjcmVkZW50aWFsLzM3MzMiLAogICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgIlZlcmlmaWFibGVDcmVkZW50aWFsIiwKICAgICAgICAgICAgICAgICJFbmRvcnNlbWVudENyZWRlbnRpYWwiCiAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAibmFtZSI6ICJTREUgZW5kb3JzZW1lbnQiLAogICAgICAgICAgICAgICJpc3N1ZXIiOiB7CiAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9zdGF0ZS5nb3YvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICJuYW1lIjogIlN0YXRlIERlcGFydG1lbnQgb2YgRWR1Y2F0aW9uIgogICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgInZhbGlkRnJvbSI6ICIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsCiAgICAgICAgICAgICAgInZhbGlkVW50aWwiOiAiMjAyMC0wMS0wMVQwMDowMDowMFoiLAogICAgICAgICAgICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgICAgICAgICAgICJpZCI6ICJodHRwczovLzFlZHRlY2guZWR1L2lzc3VlcnMvNTY1MDQ5IiwKICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAiRW5kb3JzZW1lbnRTdWJqZWN0IgogICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICJlbmRvcnNlbWVudENvbW1lbnQiOiAiMUVkVGVjaCBVbml2ZXJzaXR5IGlzIGluIGdvb2Qgc3RhbmRpbmciCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAiY3JlZGVudGlhbFNjaGVtYSI6IFsKICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9zY2hlbWEvanNvbi9vYl92M3AwX2VuZG9yc2VtZW50Y3JlZGVudGlhbF9zY2hlbWEuanNvbiIsCiAgICAgICAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hKc29uU2NoZW1hVmFsaWRhdG9yMjAxOSIKICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICJpZCI6ICJodHRwczovL3N0YXRlLmdvdi9zY2hlbWEvZW5kb3JzZW1lbnRjcmVkZW50aWFsLmpzb24iLAogICAgICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoSnNvblNjaGVtYVZhbGlkYXRvcjIwMTkiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAiY3JlZGVudGlhbFN0YXR1cyI6IHsKICAgICAgICAgICAgICAgICJpZCI6ICJodHRwczovL3N0YXRlLmdvdi9jcmVkZW50aWFscy8zNzMyL3Jldm9jYXRpb25zIiwKICAgICAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hSZXZvY2F0aW9uTGlzdCIKICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICJyZWZyZXNoU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICJpZCI6ICJodHRwOi8vc3RhdGUuZ292L2NyZWRlbnRpYWxzLzM3MzIiLAogICAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaENyZWRlbnRpYWxSZWZyZXNoIgogICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgInByb29mIjogWwogICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICAgICAgICAgICAiY3J5cHRvc3VpdGUiOiAiZWRkc2EtcmRmLTIwMjIiLAogICAgICAgICAgICAgICAgICAiY3JlYXRlZCI6ICIyMDIyLTA1LTI2VDE4OjI1OjU5WiIsCiAgICAgICAgICAgICAgICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSN6NWJEbm1TZ0Rjelh3Wkd5YTZaanhLYXhrZEt4enNDTWlWU3NnRVZXeG5hV0s3WnFiS256Y0NkN21VS0U5RFFhQUwyUU1YUDVBcXVQZVc2VzJDV3JaN2pOQyIsCiAgICAgICAgICAgICAgICAgICJwcm9vZlB1cnBvc2UiOiAiYXNzZXJ0aW9uTWV0aG9kIiwKICAgICAgICAgICAgICAgICAgInByb29mVmFsdWUiOiAiejViRG5tU2dEY3pYd1pHeWE2Wmp4S2F4a2RLeHpzQ01pVlNzZ0VWV3huYVdLN1pxYktuemNDZDdtVUtFOURRYUFMMlFNWFA1QXF1UGVXNlcyQ1dyWjdqTkMiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgXQogICAgICAgICAgICB9CiAgICAgICAgICBdLAogICAgICAgICAgImltYWdlIjogewogICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9sb2dvLnBuZyIsCiAgICAgICAgICAgICJ0eXBlIjogIkltYWdlIiwKICAgICAgICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IGxvZ28iCiAgICAgICAgICB9LAogICAgICAgICAgImVtYWlsIjogInJlZ2lzdHJhckAxZWR0ZWNoLmVkdSIsCiAgICAgICAgICAiYWRkcmVzcyI6IHsKICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgIkFkZHJlc3MiCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJhZGRyZXNzQ291bnRyeSI6ICJVU0EiLAogICAgICAgICAgICAiYWRkcmVzc0NvdW50cnlDb2RlIjogIlVTIiwKICAgICAgICAgICAgImFkZHJlc3NSZWdpb24iOiAiVFgiLAogICAgICAgICAgICAiYWRkcmVzc0xvY2FsaXR5IjogIkF1c3RpbiIsCiAgICAgICAgICAgICJzdHJlZXRBZGRyZXNzIjogIjEyMyBGaXJzdCBTdCIsCiAgICAgICAgICAgICJwb3N0T2ZmaWNlQm94TnVtYmVyIjogIjEiLAogICAgICAgICAgICAicG9zdGFsQ29kZSI6ICIxMjM0NSIsCiAgICAgICAgICAgICJnZW8iOiB7CiAgICAgICAgICAgICAgInR5cGUiOiAiR2VvQ29vcmRpbmF0ZXMiLAogICAgICAgICAgICAgICJsYXRpdHVkZSI6IDEsCiAgICAgICAgICAgICAgImxvbmdpdHVkZSI6IDEKICAgICAgICAgICAgfQogICAgICAgICAgfSwKICAgICAgICAgICJvdGhlcklkZW50aWZpZXIiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAidHlwZSI6ICJJZGVudGlmaWVyRW50cnkiLAogICAgICAgICAgICAgICJpZGVudGlmaWVyIjogIjEyMzQ1IiwKICAgICAgICAgICAgICAiaWRlbnRpZmllclR5cGUiOiAic291cmNlZElkIgogICAgICAgICAgICB9LAogICAgICAgICAgICB7CiAgICAgICAgICAgICAgInR5cGUiOiAiSWRlbnRpZmllckVudHJ5IiwKICAgICAgICAgICAgICAiaWRlbnRpZmllciI6ICI2Nzg5MCIsCiAgICAgICAgICAgICAgImlkZW50aWZpZXJUeXBlIjogIm5hdGlvbmFsSWRlbnRpdHlOdW1iZXIiCiAgICAgICAgICAgIH0KICAgICAgICAgIF0sCiAgICAgICAgICAib2ZmaWNpYWwiOiAiSG9yYWNlIE1hbm4iLAogICAgICAgICAgInBhcmVudE9yZyI6IHsKICAgICAgICAgICAgImlkIjogImRpZDpleGFtcGxlOjEyMzQ1Njc4OSIsCiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgICBdLAogICAgICAgICAgICAibmFtZSI6ICJVbml2ZXJzYWwgVW5pdmVyc2l0aWVzIgogICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgImNyZWRpdHNBdmFpbGFibGUiOiAzNiwKICAgICAgICAiY3JpdGVyaWEiOiB7CiAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9hY2hpZXZlbWVudHMvZGVncmVlIiwKICAgICAgICAgICJuYXJyYXRpdmUiOiAiIyBEZWdyZWUgUmVxdWlyZW1lbnRzXG5TdHVkZW50cyBtdXN0IGNvbXBsZXRlLi4uIgogICAgICAgIH0sCiAgICAgICAgImRlc2NyaXB0aW9uIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUgRGVzY3JpcHRpb24iLAogICAgICAgICJlbmRvcnNlbWVudCI6IFsKICAgICAgICAgIHsKICAgICAgICAgICAgIkBjb250ZXh0IjogWwogICAgICAgICAgICAgICJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLAogICAgICAgICAgICAgICJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL29iL3YzcDAvY29udGV4dC0zLjAuMy5qc29uIgogICAgICAgICAgICBdLAogICAgICAgICAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2VuZG9yc2VtZW50Y3JlZGVudGlhbC8zNzM0IiwKICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgIlZlcmlmaWFibGVDcmVkZW50aWFsIiwKICAgICAgICAgICAgICAiRW5kb3JzZW1lbnRDcmVkZW50aWFsIgogICAgICAgICAgICBdLAogICAgICAgICAgICAibmFtZSI6ICJFQUEgZW5kb3JzZW1lbnQiLAogICAgICAgICAgICAiaXNzdWVyIjogewogICAgICAgICAgICAgICJpZCI6ICJodHRwczovL2FjY3JlZGl0ZXIuZWR1L2lzc3VlcnMvNTY1MDQ5IiwKICAgICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgIm5hbWUiOiAiRXhhbXBsZSBBY2NyZWRpdGluZyBBZ2VuY3kiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJ2YWxpZEZyb20iOiAiMjAxMC0wMS0wMVQwMDowMDowMFoiLAogICAgICAgICAgICAidmFsaWRVbnRpbCI6ICIyMDIwLTAxLTAxVDAwOjAwOjAwWiIsCiAgICAgICAgICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9pc3N1ZXJzLzU2NTA0OSIsCiAgICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgICAiRW5kb3JzZW1lbnRTdWJqZWN0IgogICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgImVuZG9yc2VtZW50Q29tbWVudCI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgaXMgaW4gZ29vZCBzdGFuZGluZyIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgImNyZWRlbnRpYWxTY2hlbWEiOiBbCiAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9zY2hlbWEvanNvbi9vYl92M3AwX2VuZG9yc2VtZW50Y3JlZGVudGlhbF9zY2hlbWEuanNvbiIsCiAgICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoSnNvblNjaGVtYVZhbGlkYXRvcjIwMTkiCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9zY2hlbWEvZW5kb3JzZW1lbnRjcmVkZW50aWFsLmpzb24iLAogICAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaEpzb25TY2hlbWFWYWxpZGF0b3IyMDE5IgogICAgICAgICAgICAgIH0KICAgICAgICAgICAgXSwKICAgICAgICAgICAgImNyZWRlbnRpYWxTdGF0dXMiOiB7CiAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMi9yZXZvY2F0aW9ucyIsCiAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaFJldm9jYXRpb25MaXN0IgogICAgICAgICAgICB9LAogICAgICAgICAgICAicmVmcmVzaFNlcnZpY2UiOiB7CiAgICAgICAgICAgICAgImlkIjogImh0dHA6Ly8xZWR0ZWNoLmVkdS9jcmVkZW50aWFscy8zNzMyIiwKICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoQ3JlZGVudGlhbFJlZnJlc2giCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJwcm9vZiI6IFsKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICAgICAgICAgImNyeXB0b3N1aXRlIjogImVkZHNhLXJkZi0yMDIyIiwKICAgICAgICAgICAgICAgICJjcmVhdGVkIjogIjIwMjItMDUtMjZUMTg6MTc6MDhaIiwKICAgICAgICAgICAgICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSN6dlBrUWlVRmZKcmduQ1JoeVBrVFNrZ3JHWGJuTFIxNXBISDVIWlZZTmRNNFRDQXdRSHFHN2ZNZU1QTHRZTlJuRWdvVjFhSmRSNUU2MWVXdTVzV1JZZ3RBIiwKICAgICAgICAgICAgICAgICJwcm9vZlB1cnBvc2UiOiAiYXNzZXJ0aW9uTWV0aG9kIiwKICAgICAgICAgICAgICAgICJwcm9vZlZhbHVlIjogInp2UGtRaVVGZkpyZ25DUmh5UGtUU2tnckdYYm5MUjE1cEhINUhaVllOZE00VENBd1FIcUc3Zk1lTVBMdFlOUm5FZ29WMWFKZFI1RTYxZVd1NXNXUllndEEiCiAgICAgICAgICAgICAgfQogICAgICAgICAgICBdCiAgICAgICAgICB9CiAgICAgICAgXSwKICAgICAgICAiZmllbGRPZlN0dWR5IjogIlJlc2VhcmNoIiwKICAgICAgICAiaHVtYW5Db2RlIjogIlIxIiwKICAgICAgICAiaW1hZ2UiOiB7CiAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2hhbXJ0L2NyZWRlbnRpYWwtY29udmVydGVyL3JlZnMvaGVhZHMvaW1hZ2UvdGVzdC9lZHViYWRnZXNfMTAweDEwMC5wbmciLAogICAgICAgICAgInR5cGUiOiAiSW1hZ2UiLAogICAgICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IERlZ3JlZSIKICAgICAgICB9LAogICAgICAgICJuYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUiLAogICAgICAgICJvdGhlcklkZW50aWZpZXIiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0eXBlIjogIklkZW50aWZpZXJFbnRyeSIsCiAgICAgICAgICAgICJpZGVudGlmaWVyIjogImFiZGUiLAogICAgICAgICAgICAiaWRlbnRpZmllclR5cGUiOiAiaWRlbnRpZmllciIKICAgICAgICAgIH0KICAgICAgICBdLAogICAgICAgICJyZXN1bHREZXNjcmlwdGlvbiI6IFsKICAgICAgICAgIHsKICAgICAgICAgICAgImlkIjogInVybjp1dWlkOmY2YWIyNGNkLTg2ZTgtNGVhZi1iOGM2LWRlZDc0ZThmZDQxYyIsCiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJSZXN1bHREZXNjcmlwdGlvbiIKICAgICAgICAgICAgXSwKICAgICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgICAidGFyZ2V0Q29kZSI6ICJwcm9qZWN0IiwKICAgICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAgICJ0YXJnZXROYW1lIjogIkZpbmFsIFByb2plY3QiLAogICAgICAgICAgICAgICAgInRhcmdldEZyYW1ld29yayI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgUHJvZ3JhbSBhbmQgQ291cnNlIENhdGFsb2ciLAogICAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZJdGVtIiwKICAgICAgICAgICAgICAgICJ0YXJnZXRVcmwiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9jYXRhbG9nL2RlZ3JlZS9wcm9qZWN0IgogICAgICAgICAgICAgIH0KICAgICAgICAgICAgXSwKICAgICAgICAgICAgImFsbG93ZWRWYWx1ZSI6IFsKICAgICAgICAgICAgICAiRCIsCiAgICAgICAgICAgICAgIkMiLAogICAgICAgICAgICAgICJCIiwKICAgICAgICAgICAgICAiQSIKICAgICAgICAgICAgXSwKICAgICAgICAgICAgIm5hbWUiOiAiRmluYWwgUHJvamVjdCBHcmFkZSIsCiAgICAgICAgICAgICJyZXF1aXJlZFZhbHVlIjogIkMiLAogICAgICAgICAgICAicmVzdWx0VHlwZSI6ICJMZXR0ZXJHcmFkZSIKICAgICAgICAgIH0sCiAgICAgICAgICB7CiAgICAgICAgICAgICJpZCI6ICJ1cm46dXVpZDphNzBkZGM2YS00YzRhLTRiZDgtODI3Ny1jYjk3Yzc5ZjQwYzUiLAogICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAiUmVzdWx0RGVzY3JpcHRpb24iCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJhbGlnbm1lbnQiOiBbCiAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgICAgICJBbGlnbm1lbnQiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgInRhcmdldENvZGUiOiAicHJvamVjdCIsCiAgICAgICAgICAgICAgICAidGFyZ2V0RGVzY3JpcHRpb24iOiAiUHJvamVjdCBkZXNjcmlwdGlvbiIsCiAgICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAgICJ0YXJnZXRGcmFtZXdvcmsiOiAiMUVkVGVjaCBVbml2ZXJzaXR5IFByb2dyYW0gYW5kIENvdXJzZSBDYXRhbG9nIiwKICAgICAgICAgICAgICAgICJ0YXJnZXRUeXBlIjogIkNGSXRlbSIsCiAgICAgICAgICAgICAgICAidGFyZ2V0VXJsIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY2F0YWxvZy9kZWdyZWUvcHJvamVjdCIKICAgICAgICAgICAgICB9CiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJhbGxvd2VkVmFsdWUiOiBbCiAgICAgICAgICAgICAgIkQiLAogICAgICAgICAgICAgICJDIiwKICAgICAgICAgICAgICAiQiIsCiAgICAgICAgICAgICAgIkEiCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJuYW1lIjogIkZpbmFsIFByb2plY3QgR3JhZGUiLAogICAgICAgICAgICAicmVxdWlyZWRMZXZlbCI6ICJ1cm46dXVpZDpkMDVhMDg2Ny1kMGFkLTRiMDMtYmRiNS0yOGZiNWQyYWFiN2EiLAogICAgICAgICAgICAicmVzdWx0VHlwZSI6ICJSdWJyaWNDcml0ZXJpb25MZXZlbCIsCiAgICAgICAgICAgICJydWJyaWNDcml0ZXJpb25MZXZlbCI6IFsKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAiaWQiOiAidXJuOnV1aWQ6ZDA1YTA4NjctZDBhZC00YjAzLWJkYjUtMjhmYjVkMmFhYjdhIiwKICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAiUnVicmljQ3JpdGVyaW9uTGV2ZWwiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogInByb2plY3QiLAogICAgICAgICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZSdWJyaWNDcml0ZXJpb25MZXZlbCIsCiAgICAgICAgICAgICAgICAgICAgInRhcmdldFVybCI6ICJodHRwczovLzFlZHRlY2guZWR1L2NhdGFsb2cvZGVncmVlL3Byb2plY3QvcnVicmljL2xldmVscy9tYXN0ZXJlZCIKICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJUaGUgYXV0aG9yIGRlbW9uc3RyYXRlZC4uLiIsCiAgICAgICAgICAgICAgICAibGV2ZWwiOiAiTWFzdGVyZWQiLAogICAgICAgICAgICAgICAgIm5hbWUiOiAiTWFzdGVyeSIsCiAgICAgICAgICAgICAgICAicG9pbnRzIjogIjQiCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAiaWQiOiAidXJuOnV1aWQ6NmI4NGI0MjktMzFlZS00ZGFjLTlkMjAtZTVjNTU4ODFmODBlIiwKICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAiUnVicmljQ3JpdGVyaW9uTGV2ZWwiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogInByb2plY3QiLAogICAgICAgICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZSdWJyaWNDcml0ZXJpb25MZXZlbCIsCiAgICAgICAgICAgICAgICAgICAgInRhcmdldFVybCI6ICJodHRwczovLzFlZHRlY2guZWR1L2NhdGFsb2cvZGVncmVlL3Byb2plY3QvcnVicmljL2xldmVscy9iYXNpYyIKICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJUaGUgYXV0aG9yIGRlbW9uc3RyYXRlZC4uLiIsCiAgICAgICAgICAgICAgICAibGV2ZWwiOiAiQmFzaWMiLAogICAgICAgICAgICAgICAgIm5hbWUiOiAiQmFzaWMiLAogICAgICAgICAgICAgICAgInBvaW50cyI6ICI0IgogICAgICAgICAgICAgIH0KICAgICAgICAgICAgXQogICAgICAgICAgfSwKICAgICAgICAgIHsKICAgICAgICAgICAgImlkIjogInVybjp1dWlkOmIwN2MwMzg3LWYyZDYtNGI2NS1hM2Y0LWY0ZTQzMDJlYThmNyIsCiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJSZXN1bHREZXNjcmlwdGlvbiIKICAgICAgICAgICAgXSwKICAgICAgICAgICAgIm5hbWUiOiAiUHJvamVjdCBTdGF0dXMiLAogICAgICAgICAgICAicmVzdWx0VHlwZSI6ICJTdGF0dXMiCiAgICAgICAgICB9CiAgICAgICAgXSwKICAgICAgICAic3BlY2lhbGl6YXRpb24iOiAiQ29tcHV0ZXIgU2NpZW5jZSBSZXNlYXJjaCIsCiAgICAgICAgInRhZyI6IFsKICAgICAgICAgICJyZXNlYXJjaCIsCiAgICAgICAgICAiY29tcHV0ZXIgc2NpZW5jZSIKICAgICAgICBdCiAgICAgIH0sCiAgICAgICJpbWFnZSI6IHsKICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9jcmVkZW50aWFscy8zNzMyL2ltYWdlIiwKICAgICAgICAidHlwZSI6ICJJbWFnZSIsCiAgICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IERlZ3JlZSBmb3IgRXhhbXBsZSBTdHVkZW50IgogICAgICB9LAogICAgICAibmFycmF0aXZlIjogIlRoZXJlIGlzIGEgZmluYWwgcHJvamVjdCByZXBvcnQgYW5kIHNvdXJjZSBjb2RlIGV2aWRlbmNlLiIsCiAgICAgICJyZXN1bHQiOiBbCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICJSZXN1bHQiCiAgICAgICAgICBdLAogICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgewogICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogInByb2plY3QiLAogICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZJdGVtIiwKICAgICAgICAgICAgICAidGFyZ2V0VXJsIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY2F0YWxvZy9kZWdyZWUvcHJvamVjdC9yZXN1bHQvMSIKICAgICAgICAgICAgfQogICAgICAgICAgXSwKICAgICAgICAgICJyZXN1bHREZXNjcmlwdGlvbiI6ICJ1cm46dXVpZDpmNmFiMjRjZC04NmU4LTRlYWYtYjhjNi1kZWQ3NGU4ZmQ0MWMiLAogICAgICAgICAgInZhbHVlIjogIkEiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgIlJlc3VsdCIKICAgICAgICAgIF0sCiAgICAgICAgICAiYWNoaWV2ZWRMZXZlbCI6ICJ1cm46dXVpZDpkMDVhMDg2Ny1kMGFkLTRiMDMtYmRiNS0yOGZiNWQyYWFiN2EiLAogICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgewogICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogInByb2plY3QiLAogICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZJdGVtIiwKICAgICAgICAgICAgICAidGFyZ2V0VXJsIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY2F0YWxvZy9kZWdyZWUvcHJvamVjdC9yZXN1bHQvMSIKICAgICAgICAgICAgfQogICAgICAgICAgXSwKICAgICAgICAgICJyZXN1bHREZXNjcmlwdGlvbiI6ICJ1cm46dXVpZDpmNmFiMjRjZC04NmU4LTRlYWYtYjhjNi1kZWQ3NGU4ZmQ0MWMiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgIlJlc3VsdCIKICAgICAgICAgIF0sCiAgICAgICAgICAicmVzdWx0RGVzY3JpcHRpb24iOiAidXJuOnV1aWQ6ZjZhYjI0Y2QtODZlOC00ZWFmLWI4YzYtZGVkNzRlOGZkNDFjIiwKICAgICAgICAgICJzdGF0dXMiOiAiQ29tcGxldGVkIgogICAgICAgIH0KICAgICAgXQogICAgfSwKICAgICJlbmRvcnNlbWVudCI6IFsKICAgICAgewogICAgICAgICJAY29udGV4dCI6IFsKICAgICAgICAgICJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLAogICAgICAgICAgImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9jb250ZXh0LTMuMC4zLmpzb24iCiAgICAgICAgXSwKICAgICAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2VuZG9yc2VtZW50Y3JlZGVudGlhbC8zNzM1IiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsCiAgICAgICAgICAiRW5kb3JzZW1lbnRDcmVkZW50aWFsIgogICAgICAgIF0sCiAgICAgICAgIm5hbWUiOiAiRUFBIGVuZG9yc2VtZW50IiwKICAgICAgICAiaXNzdWVyIjogewogICAgICAgICAgImlkIjogImh0dHBzOi8vYWNjcmVkaXRlci5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgXSwKICAgICAgICAgICJuYW1lIjogIkV4YW1wbGUgQWNjcmVkaXRpbmcgQWdlbmN5IgogICAgICAgIH0sCiAgICAgICAgInZhbGlkRnJvbSI6ICIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsCiAgICAgICAgInZhbGlkVW50aWwiOiAiMjAyMC0wMS0wMVQwMDowMDowMFoiLAogICAgICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgICAgICJpZCI6ICJodHRwczovLzFlZHRlY2guZWR1L2lzc3VlcnMvNTY1MDQ5IiwKICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAiRW5kb3JzZW1lbnRTdWJqZWN0IgogICAgICAgICAgXSwKICAgICAgICAgICJlbmRvcnNlbWVudENvbW1lbnQiOiAiMUVkVGVjaCBVbml2ZXJzaXR5IGlzIGluIGdvb2Qgc3RhbmRpbmciCiAgICAgICAgfSwKICAgICAgICAiY3JlZGVudGlhbFNjaGVtYSI6IFsKICAgICAgICAgIHsKICAgICAgICAgICAgImlkIjogImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9zY2hlbWEvanNvbi9vYl92M3AwX2VuZG9yc2VtZW50Y3JlZGVudGlhbF9zY2hlbWEuanNvbiIsCiAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hKc29uU2NoZW1hVmFsaWRhdG9yMjAxOSIKICAgICAgICAgIH0sCiAgICAgICAgICB7CiAgICAgICAgICAgICJpZCI6ICJodHRwczovL2FjY3JlZGl0ZXIuZWR1L3NjaGVtYS9lbmRvcnNlbWVudGNyZWRlbnRpYWwuanNvbiIsCiAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hKc29uU2NoZW1hVmFsaWRhdG9yMjAxOSIKICAgICAgICAgIH0KICAgICAgICBdLAogICAgICAgICJjcmVkZW50aWFsU3RhdHVzIjogewogICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMi9yZXZvY2F0aW9ucyIsCiAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoUmV2b2NhdGlvbkxpc3QiCiAgICAgICAgfSwKICAgICAgICAicmVmcmVzaFNlcnZpY2UiOiB7CiAgICAgICAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2NyZWRlbnRpYWxzLzM3MzIiLAogICAgICAgICAgInR5cGUiOiAiMUVkVGVjaENyZWRlbnRpYWxSZWZyZXNoIgogICAgICAgIH0sCiAgICAgICAgInByb29mIjogWwogICAgICAgICAgewogICAgICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICAgICAiY3J5cHRvc3VpdGUiOiAiZWRkc2EtcmRmLTIwMjIiLAogICAgICAgICAgICAiY3JlYXRlZCI6ICIyMDIyLTA1LTI2VDE4OjE3OjA4WiIsCiAgICAgICAgICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSN6dlBrUWlVRmZKcmduQ1JoeVBrVFNrZ3JHWGJuTFIxNXBISDVIWlZZTmRNNFRDQXdRSHFHN2ZNZU1QTHRZTlJuRWdvVjFhSmRSNUU2MWVXdTVzV1JZZ3RBIiwKICAgICAgICAgICAgInByb29mUHVycG9zZSI6ICJhc3NlcnRpb25NZXRob2QiLAogICAgICAgICAgICAicHJvb2ZWYWx1ZSI6ICJ6dlBrUWlVRmZKcmduQ1JoeVBrVFNrZ3JHWGJuTFIxNXBISDVIWlZZTmRNNFRDQXdRSHFHN2ZNZU1QTHRZTlJuRWdvVjFhSmRSNUU2MWVXdTVzV1JZZ3RBIgogICAgICAgICAgfQogICAgICAgIF0KICAgICAgfQogICAgXSwKICAgICJldmlkZW5jZSI6IFsKICAgICAgewogICAgICAgICJpZCI6ICJodHRwczovLzFlZHRlY2guZWR1L2NyZWRlbnRpYWxzLzM3MzIvZXZpZGVuY2UvMSIsCiAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAiRXZpZGVuY2UiCiAgICAgICAgXSwKICAgICAgICAibmFycmF0aXZlIjogIiMgRmluYWwgUHJvamVjdCBSZXBvcnQgXG4gVGhpcyBwcm9qZWN0IHdhcyAuLi4iLAogICAgICAgICJuYW1lIjogIkZpbmFsIFByb2plY3QgUmVwb3J0IiwKICAgICAgICAiZGVzY3JpcHRpb24iOiAiVGhpcyBpcyB0aGUgZmluYWwgcHJvamVjdCByZXBvcnQuIiwKICAgICAgICAiZ2VucmUiOiAiUmVzZWFyY2giLAogICAgICAgICJhdWRpZW5jZSI6ICJEZXBhcnRtZW50IgogICAgICB9LAogICAgICB7CiAgICAgICAgImlkIjogImh0dHBzOi8vZ2l0aHViLmNvbS9zb21lYm9keS9wcm9qZWN0IiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJFdmlkZW5jZSIKICAgICAgICBdLAogICAgICAgICJuYW1lIjogIkZpbmFsIFByb2plY3QgQ29kZSIsCiAgICAgICAgImRlc2NyaXB0aW9uIjogIlRoaXMgaXMgdGhlIHNvdXJjZSBjb2RlIGZvciB0aGUgZmluYWwgcHJvamVjdCBhcHAuIiwKICAgICAgICAiZ2VucmUiOiAiUmVzZWFyY2giLAogICAgICAgICJhdWRpZW5jZSI6ICJEZXBhcnRtZW50IgogICAgICB9CiAgICBdLAogICAgImlzc3VlciI6IHsKICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAidHlwZSI6IFsKICAgICAgICAiUHJvZmlsZSIKICAgICAgXSwKICAgICAgIm5hbWUiOiAiMUVkVGVjaCBVbml2ZXJzaXR5IiwKICAgICAgInVybCI6ICJodHRwczovLzFlZHRlY2guZWR1IiwKICAgICAgInBob25lIjogIjEtMjIyLTMzMy00NDQ0IiwKICAgICAgImRlc2NyaXB0aW9uIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBwcm92aWRlcyBvbmxpbmUgZGVncmVlIHByb2dyYW1zLiIsCiAgICAgICJlbmRvcnNlbWVudCI6IFsKICAgICAgICB7CiAgICAgICAgICAiQGNvbnRleHQiOiBbCiAgICAgICAgICAgICJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLAogICAgICAgICAgICAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjMuanNvbiIKICAgICAgICAgIF0sCiAgICAgICAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2VuZG9yc2VtZW50Y3JlZGVudGlhbC8zNzM2IiwKICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLAogICAgICAgICAgICAiRW5kb3JzZW1lbnRDcmVkZW50aWFsIgogICAgICAgICAgXSwKICAgICAgICAgICJuYW1lIjogIkVBQSBlbmRvcnNlbWVudCIsCiAgICAgICAgICAiaXNzdWVyIjogewogICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSIsCiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgICBdLAogICAgICAgICAgICAibmFtZSI6ICJFeGFtcGxlIEFjY3JlZGl0aW5nIEFnZW5jeSIKICAgICAgICAgIH0sCiAgICAgICAgICAidmFsaWRGcm9tIjogIjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwKICAgICAgICAgICJ2YWxpZFVudGlsIjogIjIwMjAtMDEtMDFUMDA6MDA6MDBaIiwKICAgICAgICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAiRW5kb3JzZW1lbnRTdWJqZWN0IgogICAgICAgICAgICBdLAogICAgICAgICAgICAiZW5kb3JzZW1lbnRDb21tZW50IjogIjFFZFRlY2ggVW5pdmVyc2l0eSBpcyBpbiBnb29kIHN0YW5kaW5nIgogICAgICAgICAgfSwKICAgICAgICAgICJjcmVkZW50aWFsU2NoZW1hIjogWwogICAgICAgICAgICB7CiAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9zY2hlbWEvanNvbi9vYl92M3AwX2VuZG9yc2VtZW50Y3JlZGVudGlhbF9zY2hlbWEuanNvbiIsCiAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaEpzb25TY2hlbWFWYWxpZGF0b3IyMDE5IgogICAgICAgICAgICB9LAogICAgICAgICAgICB7CiAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vYWNjcmVkaXRlci5lZHUvc2NoZW1hL2VuZG9yc2VtZW50Y3JlZGVudGlhbC5qc29uIiwKICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoSnNvblNjaGVtYVZhbGlkYXRvcjIwMTkiCiAgICAgICAgICAgIH0KICAgICAgICAgIF0sCiAgICAgICAgICAiY3JlZGVudGlhbFN0YXR1cyI6IHsKICAgICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMi9yZXZvY2F0aW9ucyIsCiAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hSZXZvY2F0aW9uTGlzdCIKICAgICAgICAgIH0sCiAgICAgICAgICAicmVmcmVzaFNlcnZpY2UiOiB7CiAgICAgICAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMiIsCiAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hDcmVkZW50aWFsUmVmcmVzaCIKICAgICAgICAgIH0sCiAgICAgICAgICAicHJvb2YiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICAgICAgICJjcnlwdG9zdWl0ZSI6ICJlZGRzYS1yZGYtMjAyMiIsCiAgICAgICAgICAgICAgImNyZWF0ZWQiOiAiMjAyMi0wNS0yNlQxODoxNzowOFoiLAogICAgICAgICAgICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSN6dlBrUWlVRmZKcmduQ1JoeVBrVFNrZ3JHWGJuTFIxNXBISDVIWlZZTmRNNFRDQXdRSHFHN2ZNZU1QTHRZTlJuRWdvVjFhSmRSNUU2MWVXdTVzV1JZZ3RBIiwKICAgICAgICAgICAgICAicHJvb2ZQdXJwb3NlIjogImFzc2VydGlvbk1ldGhvZCIsCiAgICAgICAgICAgICAgInByb29mVmFsdWUiOiAienZQa1FpVUZmSnJnbkNSaHlQa1RTa2dyR1hibkxSMTVwSEg1SFpWWU5kTTRUQ0F3UUhxRzdmTWVNUEx0WU5SbkVnb1YxYUpkUjVFNjFlV3U1c1dSWWd0QSIKICAgICAgICAgICAgfQogICAgICAgICAgXQogICAgICAgIH0KICAgICAgXSwKICAgICAgImltYWdlIjogewogICAgICAgICJpZCI6ICJodHRwczovLzFlZHRlY2guZWR1L2xvZ28ucG5nIiwKICAgICAgICAidHlwZSI6ICJJbWFnZSIsCiAgICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IGxvZ28iCiAgICAgIH0sCiAgICAgICJlbWFpbCI6ICJyZWdpc3RyYXJAMWVkdGVjaC5lZHUiLAogICAgICAiYWRkcmVzcyI6IHsKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJBZGRyZXNzIgogICAgICAgIF0sCiAgICAgICAgImFkZHJlc3NDb3VudHJ5IjogIlVTQSIsCiAgICAgICAgImFkZHJlc3NDb3VudHJ5Q29kZSI6ICJVUyIsCiAgICAgICAgImFkZHJlc3NSZWdpb24iOiAiVFgiLAogICAgICAgICJhZGRyZXNzTG9jYWxpdHkiOiAiQXVzdGluIiwKICAgICAgICAic3RyZWV0QWRkcmVzcyI6ICIxMjMgRmlyc3QgU3QiLAogICAgICAgICJwb3N0T2ZmaWNlQm94TnVtYmVyIjogIjEiLAogICAgICAgICJwb3N0YWxDb2RlIjogIjEyMzQ1IiwKICAgICAgICAiZ2VvIjogewogICAgICAgICAgInR5cGUiOiAiR2VvQ29vcmRpbmF0ZXMiLAogICAgICAgICAgImxhdGl0dWRlIjogMSwKICAgICAgICAgICJsb25naXR1ZGUiOiAxCiAgICAgICAgfQogICAgICB9LAogICAgICAib3RoZXJJZGVudGlmaWVyIjogWwogICAgICAgIHsKICAgICAgICAgICJ0eXBlIjogIklkZW50aWZpZXJFbnRyeSIsCiAgICAgICAgICAiaWRlbnRpZmllciI6ICIxMjM0NSIsCiAgICAgICAgICAiaWRlbnRpZmllclR5cGUiOiAic291cmNlZElkIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiAiSWRlbnRpZmllckVudHJ5IiwKICAgICAgICAgICJpZGVudGlmaWVyIjogIjY3ODkwIiwKICAgICAgICAgICJpZGVudGlmaWVyVHlwZSI6ICJuYXRpb25hbElkZW50aXR5TnVtYmVyIgogICAgICAgIH0KICAgICAgXSwKICAgICAgIm9mZmljaWFsIjogIkhvcmFjZSBNYW5uIiwKICAgICAgInBhcmVudE9yZyI6IHsKICAgICAgICAiaWQiOiAiZGlkOmV4YW1wbGU6MTIzNDU2Nzg5IiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJQcm9maWxlIgogICAgICAgIF0sCiAgICAgICAgIm5hbWUiOiAiVW5pdmVyc2FsIFVuaXZlcnNpdGllcyIKICAgICAgfQogICAgfSwKICAgICJ2YWxpZEZyb20iOiAiMjAxMC0wMS0wMVQwMDowMDowMFoiLAogICAgInZhbGlkVW50aWwiOiAiMjAzMC0wMS0wMVQwMDowMDowMFoiLAogICAgImNyZWRlbnRpYWxTY2hlbWEiOiBbCiAgICAgIHsKICAgICAgICAiaWQiOiAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL3NjaGVtYS9qc29uL29iX3YzcDBfYWNoaWV2ZW1lbnRjcmVkZW50aWFsX3NjaGVtYS5qc29uIiwKICAgICAgICAidHlwZSI6ICIxRWRUZWNoSnNvblNjaGVtYVZhbGlkYXRvcjIwMTkiCiAgICAgIH0KICAgIF0sCiAgICAiY3JlZGVudGlhbFN0YXR1cyI6IHsKICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMi9yZXZvY2F0aW9ucyIsCiAgICAgICJ0eXBlIjogIjFFZFRlY2hSZXZvY2F0aW9uTGlzdCIKICAgIH0sCiAgICAicmVmcmVzaFNlcnZpY2UiOiB7CiAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMiIsCiAgICAgICJ0eXBlIjogIjFFZFRlY2hDcmVkZW50aWFsUmVmcmVzaCIKICAgIH0sCiAgICAicHJvb2YiOiBbCiAgICAgIHsKICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICJjcmVhdGVkIjogIjIwMjQtMDUtMzFUMTQ6MDU6MjVaIiwKICAgICAgICAidmVyaWZpY2F0aW9uTWV0aG9kIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkjejZNa3BoVTZRbW9qQzZHZFVCTll5cGduR2FpTDJUTGlzTE14cEUxb1pjbUtnN0FkIiwKICAgICAgICAiY3J5cHRvc3VpdGUiOiAiZWRkc2EtcmRmYy0yMDIyIiwKICAgICAgICAicHJvb2ZQdXJwb3NlIjogImFzc2VydGlvbk1ldGhvZCIsCiAgICAgICAgInByb29mVmFsdWUiOiAiejVBNFpYTEphNGRVQXJUbXBkUDl2bnJZaWpNTENUMXRSOUtXYUZtTFQyUGVRcDNnU25HQTl3clJKcXJKNVo4WW5wVkR4WlFXUkdqaldOYmoyUEtESmU3ZHQiCiAgICAgIH0KICAgIF0KfQo=" +} \ No newline at end of file From 30d2771fc2f68c7e461c0986340257196b7db50f Mon Sep 17 00:00:00 2001 From: hamrt Date: Tue, 4 Feb 2025 00:38:40 +0100 Subject: [PATCH 35/45] add extra image issuer, credits and specification, fix issue API --- .../custom_mapping_OBv3_ELM_latest.json | 45 +++- .../Complete_OpenBadgeCredential_image.json | 2 +- src/backend/base64_encode.rs | 203 +++++++++++------- src/backend/elm_mapping_helper.rs | 93 ++++++++ src/backend/repository.rs | 151 ++++++++++++- src/backend/routes/api.rs | 157 +++++++++----- src/backend/transformations.rs | 61 ++++++ test/encoded_test.json | 2 +- 8 files changed, 569 insertions(+), 145 deletions(-) diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index 8c1f647..0cb4e25 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -42,6 +42,16 @@ "path": "$.issuer.id" } }, + { + "type_": "stringit", + "source": { + "value": "Organisation" + }, + "destination": { + "format": "ELM", + "path": "$.issuer.type" + } + }, { "type_": "copy", "source": { @@ -54,24 +64,25 @@ } }, { - "type_": "stringit", + "type_": "addressToLocation", "source": { - "value": "Organisation" + "format": "OBv3", + "path": "$.issuer.address" }, "destination": { "format": "ELM", - "path": "$.issuer.type" + "path": "$.issuer.location" } }, { - "type_": "addressToLocation", + "type_": "imageToMediaObject", "source": { "format": "OBv3", - "path": "$.issuer.address" + "path": "$.issuer.image" }, "destination": { "format": "ELM", - "path": "$.issuer.location" + "path": "$.issuer.logo" } }, { @@ -246,6 +257,28 @@ "path": "$.credentialSchema" } }, + { + "type_": "titleToSpecifiedByObject", + "source": { + "format": "OBv3", + "path": "$.name" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy" + } + }, + { + "type_": "creditToSpecifiedByObject", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.creditsAvailable" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.creditPoint[0]" + } + }, { "type_": "identifierToObject", "source": { diff --git a/json/obv3/examples/Complete_OpenBadgeCredential_image.json b/json/obv3/examples/Complete_OpenBadgeCredential_image.json index 59e955c..053e83e 100644 --- a/json/obv3/examples/Complete_OpenBadgeCredential_image.json +++ b/json/obv3/examples/Complete_OpenBadgeCredential_image.json @@ -651,7 +651,7 @@ } ], "image": { - "id": "https://1edtech.edu/logo.png", + "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/edubadges_100x100.png", "type": "Image", "caption": "1EdTech University logo" }, diff --git a/src/backend/base64_encode.rs b/src/backend/base64_encode.rs index 1d49608..788be0b 100644 --- a/src/backend/base64_encode.rs +++ b/src/backend/base64_encode.rs @@ -12,15 +12,32 @@ use ureq::get; /// - `string`: The string of the json to decode. /// /// # Returns -/// - `Ok(Byte)`: The Base64-encoded string of the image if successful. +/// - `Ok(Byte)`: The Base64-encoded string of the json snippet if successful. /// - `Err(Box)`: An error if the fetch or encoding fails. pub fn decode_json(json: &str) -> Result, Box> { - // Encode the image bytes as a Base64 string + // Decode the image bytes as a Base64 string let conv_byte = Base64Engine.decode(json)?; Ok(conv_byte) } + +/// Encode json input from Base64, and returns the byte array. +/// +/// # Arguments +/// - `string`: The string of the json to encode. +/// +/// # Returns +/// - `Ok(String)`: The Base64-encoded string of the json file. +/// - `Err(Box)`: An error if the fetch or encoding fails. +pub fn encode_json_file(json_file: Vec) -> Result> { + // Encode the image bytes as a Base64 string + let base64_string = Base64Engine.encode(json_file); + + Ok(base64_string) +} + + /// Fetches an image from the given URL, encodes it in Base64, and returns the encoded string. /// /// # Arguments @@ -30,18 +47,24 @@ pub fn decode_json(json: &str) -> Result, Box> { /// - `Ok(String)`: The Base64-encoded string of the image if successful. /// - `Err(Box)`: An error if the fetch or encoding fails. fn encode_image_from_url(url: &str) -> Result> { - let resp = get(url).call().expect("Failed to download image"); - - assert!(resp.has("Content-Length")); - let len: usize = resp.header("Content-Length").unwrap().parse()?; - - let mut bytes: Vec = Vec::with_capacity(len); - resp.into_reader().take(10_000_000).read_to_end(&mut bytes)?; - - // Encode the image bytes as a Base64 string - let base64_string = Base64Engine.encode(&bytes); + let base64_string: String; + match get(url).call() { + Ok(resp) => { + assert!(resp.has("Content-Length")); + let len: usize = resp.header("Content-Length").unwrap().parse()?; + + let mut bytes: Vec = Vec::with_capacity(len); + resp.into_reader().take(10_000_000).read_to_end(&mut bytes)?; + + // Encode the image bytes as a Base64 string + base64_string = Base64Engine.encode(&bytes); + return Ok(base64_string); + } + Err(e) => { + return Err(Box::new(e)); + } - Ok(base64_string) + } } /// Creates contentType object based on input type in string @@ -187,62 +210,39 @@ fn set_language(language: &str) -> Result> { Ok(parsed_language_json) } -pub fn image_to_individual_display(image_value: Value) -> Value { +pub fn image_to_elm_media_object(image_value: Value) -> Value { //inspect the image object and re write it so it can be reused in ELM //we need to achieve the following structure into the indivudualDisplay array: let json_data = r#" { - "id": "urn:epass:individualDisplay:c05743e7-9f9d-4e0b-899b-7ae6514c7a02", - "type": "IndividualDisplay", - "language": { - "id": "http://publications.europa.eu/resource/authority/language/ENG", - "type": "Concept", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/language", + "id": "urn:epass:mediaObject:https://avatars.githubusercontent.com/u/22613412?v=4", + "type": "MediaObject", + "content": "bas64content", + "contentEncoding": { + "id": "http://data.europa.eu/snb/encoding/6146cde7dd", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/encoding/25831c2", "type": "ConceptScheme" - }, - "prefLabel": { - "en": ["English"] - }, - "notation": "language" }, - "displayDetail": [ - { - "id": "urn:epass:displayDetail:123", - "type": "DisplayDetail", - "image": { - "id": "urn:epass:mediaObject:https://avatars.githubusercontent.com/u/22613412?v=4", - "type": "MediaObject", - "content": "bas64content", - "contentEncoding": { - "id": "http://data.europa.eu/snb/encoding/6146cde7dd", - "type": "Concept", - "inScheme": { - "id": "http://data.europa.eu/snb/encoding/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": ["base64"] - } - }, - "contentType": { - "id": "http://publications.europa.eu/resource/authority/file-type/JPEG", - "type": "Concept", - "inScheme": { - "id": "http://publications.europa.eu/resource/authority/file-type", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": ["JPG"] - }, - "notation": "file-type" - } - }, - "page": 1 - } - ] - } + "prefLabel": { + "en": ["base64"] + } + }, + "contentType": { + "id": "http://publications.europa.eu/resource/authority/file-type/JPEG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/file-type", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["JPG"] + }, + "notation": "file-type" + } + } "#; let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); @@ -250,7 +250,7 @@ pub fn image_to_individual_display(image_value: Value) -> Value { // OB usess the id field to point to an image or have the image encoded. // Content type is also based on either URL or encoding in the id. // Extract the `id` field - let mut encoded_string = String::new(); + let encoded_string: String; let mut file_type_sting = String::new(); if let Some(id_value) = image_value.get("id") { if let Some(ob3_image_id) = id_value.as_str() { @@ -278,9 +278,9 @@ pub fn image_to_individual_display(image_value: Value) -> Value { // println!("No file extension found."); } } - Err(e) => { - eprintln!("Error: {}", e); - encoded_string = String::new(); // Assign an empty string or a default value in case of an error + Err(_e) => { +// eprintln!("Error: {}", _e); + return Value::Null; } }; // } else if Base64Engine.decode(ob3_image_id).is_ok() { @@ -296,24 +296,28 @@ pub fn image_to_individual_display(image_value: Value) -> Value { encoded_string = content_part.to_string(); } else { // println!("Invalid data URI format."); + return Value::Null; } } else { // println!("The `id` is neither a URL nor a Base64-encoded string."); + return Value::Null; } } else { // println!("The 'id' field is not a string."); + return Value::Null; } } else { // println!("The 'id' field does not exist."); + return Value::Null; } - if let Some(_image_content) = parsed_json["displayDetail"][0]["image"]["content"].as_str() { - parsed_json["displayDetail"][0]["image"]["content"] = Value::String(encoded_string); + if let Some(_image_content) = parsed_json["content"].as_str() { + parsed_json["content"] = Value::String(encoded_string); } else { // println!("Key 'content' in 'image' not found."); } - // Directly mutate the `contentType` value + // Directly mutate the `contentType` value of the image // Set the contentType to a choosen value (currently default to PNG) if file_type_sting.is_empty() { file_type_sting = "PNG".to_string(); @@ -330,14 +334,13 @@ pub fn image_to_individual_display(image_value: Value) -> Value { } }; - if let Some(_content_type) = parsed_json["displayDetail"][0]["image"].as_object() { - parsed_json["displayDetail"][0]["image"]["contentType"] = encoded_content_type; + if let Some(_content_type) = parsed_json.as_object() { + parsed_json["contentType"] = encoded_content_type; } else { // println!("Key 'contentType' in 'image' not found."); } - // Directly mutate the `encoding` value - // first try to encode the image in the URL: + // Directly mutate the `encoding` value of the image let encoding_value = match set_content_enconding_type("base64") { Ok(encoding_value) => { // println!("Successfully added encoding type to the image."); @@ -349,14 +352,62 @@ pub fn image_to_individual_display(image_value: Value) -> Value { } }; - if let Some(_image_encoding) = parsed_json["displayDetail"][0]["image"]["contentEncoding"].as_object() { - parsed_json["displayDetail"][0]["image"]["contentEncoding"] = encoding_value; + if let Some(_image_encoding) = parsed_json["contentEncoding"].as_object() { + parsed_json["contentEncoding"] = encoding_value; } else { // println!("Key 'contentEncoding' in 'image' not found."); } +// println!("{:#?}", parsed_json); + parsed_json +} + + + +pub fn image_to_individual_display(image_value: Value) -> Value { + //inspect the image object and re write it so it can be reused in ELM + + //we need to achieve the following structure into the indivudualDisplay array: + let json_data = r#" + { + "id": "urn:epass:individualDisplay:c05743e7-9f9d-4e0b-899b-7ae6514c7a02", + "type": "IndividualDisplay", + "language": { + "id": "http://publications.europa.eu/resource/authority/language/ENG", + "type": "Concept", + "inScheme": { + "id": "http://publications.europa.eu/resource/authority/language", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["English"] + }, + "notation": "language" + }, + "displayDetail": [ + { + "id": "urn:epass:displayDetail:123", + "type": "DisplayDetail", + "image": {"object": "data"}, + "page": 1 + } + ] + } + "#; + + let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); + + // OB usess the id field to point to an image or have the image encoded. + // Content type is also based on either URL or encoding in the id. + // Extract the `id` field + let image_object_value = image_to_elm_media_object(image_value); + if let Some(_image_data) = parsed_json["displayDetail"][0]["image"].as_object() { + parsed_json["displayDetail"][0]["image"] = image_object_value; + } else { + // println!("Key 'contentType' in 'image' not found."); + } + // Directly mutate the `language` value - // first try to encode the image in the URL: let language_value = match set_language("ENG") { Ok(language_value) => { // println!("Successfully added language to the individual display properties."); diff --git a/src/backend/elm_mapping_helper.rs b/src/backend/elm_mapping_helper.rs index 0cab243..724e375 100644 --- a/src/backend/elm_mapping_helper.rs +++ b/src/backend/elm_mapping_helper.rs @@ -65,3 +65,96 @@ pub fn address_to_location(address_value: Value) -> Value { //println!("{:#?}", parsed_json); parsed_json } + + + +/// Creates specifiedBy based on input type in string found title +/// +/// # Arguments +/// - `title`: the code found in . +/// +/// # Returns +/// - Value: The content value Object in ELM format if successful. +pub fn title_to_specifiedby(title: Value) -> Value { + //inspect the title object and re write it so it can be reused in ELM for building a Specification + //we need to achieve the following structure for a specification: + let json_data = r#" + { + "id": "urn:epass:learningAchievementSpec:1", + "type": "LearningAchievementSpecification", + "title": { + "en": ["Data and soferetware business"] + } + } + "#; + + let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); + + // Directly mutate the `Location` value + // Access the "addressCountryCode" field + if let Some(title_str) = title.as_str() { + if title_str.is_empty() { + return Value::Null; + } + else{ + parsed_json["title"]["en"][0] = Value::String(title_str.to_string()); + + } + } else { + return Value::Null; + } + + + //println!("{:#?}", parsed_json); + parsed_json +} + + + + + +/// Creates specifiedBy based on input type in string found title +/// +/// # Arguments +/// - `credits`: the ammount of credits for a credential +/// +/// # Returns +/// - Value: The creditpoint value Object in ELM format if successful. +pub fn credentialpoint_values_to_object(credits: Value) -> Value { + //inspect the title object and re write it so it can be reused in ELM for building a creditpoint that cn be used in Specification + //we need to achieve the following structure for a creditpoint: + let json_data = r#" + { + "id": "urn:epass:creditPoint:1", + "type": "CreditPoint", + "framework": { + "id": "http://data.europa.eu/snb/education-credit/6fcec5c5af", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/education-credit/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["European Credit Transfer System"] + } + }, + "point": "5" + } + "#; + + let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); + + // Directly mutate the `Location` value + // Access the "addressCountryCode" field + match credits { + Value::String(_) => {parsed_json["point"] = Value::String(credits.to_string());}, + Value::Number(_) => {parsed_json["point"] = Value::String(credits.to_string());}, + _ => {return Value::Null;} + } + //println!("{:#?}", parsed_json); + parsed_json +} + + + + diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 82833a4..2d6c640 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -1,7 +1,7 @@ use crate::{ backend::{ - base64_encode::create_display_parameter, - elm_mapping_helper::address_to_location, + base64_encode::{create_display_parameter,image_to_elm_media_object}, + elm_mapping_helper::{address_to_location, title_to_specifiedby, credentialpoint_values_to_object}, jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, transformations::{DataLocation, DataTypeLocation, StringArrayValue, StringValue, Transformation}, @@ -428,6 +428,153 @@ impl Repository { Some((destination_path, source_path)) } + Transformation::ImageToMediaObject { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + + // run the source value through a markdown converter to fit the nested objects into a markdown string + // let image_individualdisplay_source = Value::Array(vec![json!(create_display_parameter(source_value))]); + let image_individualdisplay_source = json!(image_to_elm_media_object(source_value)); + if image_individualdisplay_source.is_null() { + return None; + } + + // let markdown_source_value = json!(image_to_individual_display(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(image_individualdisplay_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + + Transformation::TitleToSpecifiedByObject { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + // run the source value through a speficfiedby converter to fit the nested objects into a markdown string + let specifiedby_source = json!(title_to_specifiedby(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(specifiedby_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + Transformation::CreditToSpecifiedByObject { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + // run the source value through a speficfiedby converter to fit the nested objects into a markdown string + let cred_specifiedby_source = json!(credentialpoint_values_to_object(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(cred_specifiedby_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + Transformation::AddressToLocation { type_: transformation, source: diff --git a/src/backend/routes/api.rs b/src/backend/routes/api.rs index 8c43978..82c1e27 100644 --- a/src/backend/routes/api.rs +++ b/src/backend/routes/api.rs @@ -2,19 +2,19 @@ use axum::{ // routing::post, // Router, http::{header, HeaderMap, HeaderValue, StatusCode}, - response::{IntoResponse, Response}, + response::IntoResponse, Json, }; //use config::Value; -use serde_json::Value; +use serde_json::{Value,json}; -use crate::backend::base64_encode::decode_json; +use crate::backend::base64_encode::{decode_json,encode_json_file}; use crate::backend::headless_cli::load_files_apply_transformations; use crate::state::{AppState, Mapping}; use std::{fs::File, io::Write, path::Path}; use tokio::fs; -pub async fn api(Json(input_json): Json) -> Result { +pub async fn api(Json(input_json): Json) -> impl IntoResponse { //test the input types for this API // with JSON body: { // "From": {"Name": "OB", "Version": "3.0"}, @@ -32,18 +32,18 @@ pub async fn api(Json(input_json): Json) -> Result) -> Result return Err((StatusCode::BAD_REQUEST, format!("Invalid translation value: {}", value))), + Some(value) => { + let error_json = json!({ + "error": "Bad Request", + "message" : format!("Invalid translation value: {}", value)}); + return (StatusCode::BAD_REQUEST, Json(error_json)) + } None => { - return Err(( - StatusCode::BAD_REQUEST, - "Invalid translation value: no key found".to_string(), - )) + let error_json = json!({ + "error": "Bad Request", + "message" : "Invalid translation value: no key found"}); + return (StatusCode::BAD_REQUEST, Json(error_json)) } } match input_json.get("Content").and_then(|v| v.as_str()) { Some(value) => { _input_file_path = format!("{}/{}", upload_dir, file_name); - let mut file = File::create(&_input_file_path) - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to create file".to_string()))?; - let data = decode_json(value).map_err(|_| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - "Failed to read file data".to_string(), - ) - })?; - file.write_all(&data) - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to write to file".to_string()))?; + let file = File::create(&_input_file_path) + .map_err(|_| { + let error_json = json!({ + "error": "Internal Server Error", + "message" : "Failed to create file"}); + (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)) + }); + match decode_json(value) { + Ok(data) => { + match file { + Ok(mut file_found) => {let _ = file_found.write_all(&data);} + Err(file_error) => {return file_error;} + // Err(file_error) => {let error_json = json!({ + // "error": "Internal Server Error", + // "message" : "Failed to write to file"}); + // return (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json));} + } + } + Err(_decode_err) => { let error_json = json!({ + "error": "Internal Server Error", + "message" : "Failed to read file data"}); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)); + } + } } - None => return Err((StatusCode::BAD_REQUEST, "Invalid data value: no key found".to_string())), + + None => { + let error_json = json!({ + "error": "Bad Request", + "message" : "Invalid data value: no key found"}); + return (StatusCode::BAD_REQUEST, Json(error_json)) + } } // Define the output file path @@ -109,40 +134,54 @@ pub async fn api(Json(input_json): Json) -> Result {match encode_json_file(content) { + Ok(encoded_json) => { + let response_json = json!({"content": encoded_json}); + return (StatusCode::OK, Json(response_json)); + } + Err (_enc_error) => { + let error_json = json!({ + "error": "Internal Server Error", + "message" : "Failed to encode the json file"}); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)); + } + } + } + Err(status) => { + return status; + } + } + // let encoded_json = encode_json_file(output_file)?; - // Ok(output_file.into_response()) + // // Return the file content along with the appropriate headers + // let response_json = json!({"content": encoded_json}); + // (StatusCode::OK, Json(response_json)) + // // Ok(output_file.into_response()) } diff --git a/src/backend/transformations.rs b/src/backend/transformations.rs index 4fc42b0..8837828 100644 --- a/src/backend/transformations.rs +++ b/src/backend/transformations.rs @@ -193,6 +193,52 @@ impl ImageToIndividualDisplay { } } + +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum CreditToSpecifiedByObject { + creditToSpecifiedByObject, +} + +impl CreditToSpecifiedByObject { + pub fn apply(&self, value: Value) -> Value { + match self { + CreditToSpecifiedByObject::creditToSpecifiedByObject => value, + } + } +} + + + +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum TitleToSpecifiedByObject { + titleToSpecifiedByObject, +} + +impl TitleToSpecifiedByObject { + pub fn apply(&self, value: Value) -> Value { + match self { + TitleToSpecifiedByObject::titleToSpecifiedByObject => value, + } + } +} + + +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum ImageToMediaObject { + imageToMediaObject, +} + +impl ImageToMediaObject { + pub fn apply(&self, value: Value) -> Value { + match self { + ImageToMediaObject::imageToMediaObject => value, + } + } +} + #[allow(non_camel_case_types)] #[derive(Serialize, Deserialize, Debug, Clone)] pub enum AddressToLocation { @@ -250,6 +296,21 @@ pub enum Transformation { source: DataLocation, destination: DataLocation, }, + ImageToMediaObject { + type_: ImageToMediaObject, + source: DataLocation, + destination: DataLocation, + }, + TitleToSpecifiedByObject { + type_: TitleToSpecifiedByObject, + source: DataLocation, + destination: DataLocation, + }, + CreditToSpecifiedByObject{ + type_: CreditToSpecifiedByObject, + source: DataLocation, + destination: DataLocation, + }, AddressToLocation { type_: AddressToLocation, source: DataLocation, diff --git a/test/encoded_test.json b/test/encoded_test.json index 1a4abc8..23d51e8 100644 --- a/test/encoded_test.json +++ b/test/encoded_test.json @@ -2,5 +2,5 @@ "From": {"Name": "OB", "Version": "3.0"}, "To": {"Name": "elm", "Version": "3.2"}, "Parameters": { "PreferredLanguages": ["en", "sv"]}, - "Content" : "ewogICAgIkBjb250ZXh0IjogWwogICAgICAiaHR0cHM6Ly93d3cudzMub3JnL25zL2NyZWRlbnRpYWxzL3YyIiwKICAgICAgImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9jb250ZXh0LTMuMC4zLmpzb24iLAogICAgICAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2V4dGVuc2lvbnMuanNvbiIKICAgIF0sCiAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2NyZWRlbnRpYWxzLzM3MzIiLAogICAgInR5cGUiOiBbCiAgICAgICJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsCiAgICAgICJPcGVuQmFkZ2VDcmVkZW50aWFsIgogICAgXSwKICAgICJuYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUgZm9yIEV4YW1wbGUgU3R1ZGVudCIsCiAgICAiZGVzY3JpcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IERlZ3JlZSBEZXNjcmlwdGlvbiIsCiAgICAiaW1hZ2UiOiB7CiAgICAgICJpZCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vaGFtcnQvY3JlZGVudGlhbC1jb252ZXJ0ZXIvcmVmcy9oZWFkcy9pbWFnZS90ZXN0L2VkdWJhZGdlc18xMDB4MTAwLnBuZyIsCiAgICAgICJ0eXBlIjogIkltYWdlIiwKICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IERlZ3JlZSBmb3IgRXhhbXBsZSBTdHVkZW50IgogICAgfSwKICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgImlkIjogImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsCiAgICAgICJ0eXBlIjogWwogICAgICAgICJBY2hpZXZlbWVudFN1YmplY3QiCiAgICAgIF0sCiAgICAgICJhY3Rpdml0eUVuZERhdGUiOiAiMjAxMC0wMS0wMlQwMDowMDowMFoiLAogICAgICAiYWN0aXZpdHlTdGFydERhdGUiOiAiMjAxMC0wMS0wMVQwMDowMDowMFoiLAogICAgICAiY3JlZGl0c0Vhcm5lZCI6IDQyLAogICAgICAibGljZW5zZU51bWJlciI6ICJBLTkzMjAwNDEiLAogICAgICAicm9sZSI6ICJNYWpvciBEb21vIiwKICAgICAgInNvdXJjZSI6IHsKICAgICAgICAiaWQiOiAiaHR0cHM6Ly9zY2hvb2wuZWR1L2lzc3VlcnMvMjAxMjM0IiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJQcm9maWxlIgogICAgICAgIF0sCiAgICAgICAgIm5hbWUiOiAiMUVkVGVjaCBDb2xsZWdlIG9mIEFydHMiCiAgICAgIH0sCiAgICAgICJ0ZXJtIjogIkZhbGwiLAogICAgICAiaWRlbnRpZmllciI6IFsKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6ICJJZGVudGl0eU9iamVjdCIsCiAgICAgICAgICAiaWRlbnRpdHlIYXNoIjogInN0dWRlbnRAMWVkdGVjaC5lZHUiLAogICAgICAgICAgImlkZW50aXR5VHlwZSI6ICJlbWFpbEFkZHJlc3MiLAogICAgICAgICAgImhhc2hlZCI6IGZhbHNlLAogICAgICAgICAgInNhbHQiOiAibm90LXVzZWQiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6ICJJZGVudGl0eU9iamVjdCIsCiAgICAgICAgICAiaWRlbnRpdHlIYXNoIjogInNvbWVib2R5QGdtYWlsLmNvbSIsCiAgICAgICAgICAiaWRlbnRpdHlUeXBlIjogImVtYWlsQWRkcmVzcyIsCiAgICAgICAgICAiaGFzaGVkIjogZmFsc2UsCiAgICAgICAgICAic2FsdCI6ICJub3QtdXNlZCIKICAgICAgICB9CiAgICAgIF0sCiAgICAgICJhY2hpZXZlbWVudCI6IHsKICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9hY2hpZXZlbWVudHMvZGVncmVlIiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJBY2hpZXZlbWVudCIKICAgICAgICBdLAogICAgICAgICJhbGlnbm1lbnQiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJBbGlnbm1lbnQiCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogImRlZ3JlZSIsCiAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgRGVncmVlIHByb2dyYW1zLiIsCiAgICAgICAgICAgICJ0YXJnZXROYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUiLAogICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICJ0YXJnZXRUeXBlIjogIkNGSXRlbSIsCiAgICAgICAgICAgICJ0YXJnZXRVcmwiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9jYXRhbG9nL2RlZ3JlZSIKICAgICAgICAgIH0sCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJBbGlnbm1lbnQiCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogImRlZ3JlZSIsCiAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgRGVncmVlIHByb2dyYW1zLiIsCiAgICAgICAgICAgICJ0YXJnZXROYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUiLAogICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICJ0YXJnZXRUeXBlIjogIkNUREwiLAogICAgICAgICAgICAidGFyZ2V0VXJsIjogImh0dHBzOi8vY3JlZGVudGlhbGVuZ2luZXJlZ2lzdHJ5Lm9yZy9yZXNvdXJjZXMvY2UtOThjYjAyN2ItOTVlZi00NDk0LTkwOGQtNmY3NzkwZWM2YjZiIgogICAgICAgICAgfQogICAgICAgIF0sCiAgICAgICAgImFjaGlldmVtZW50VHlwZSI6ICJEZWdyZWUiLAogICAgICAgICJjcmVhdG9yIjogewogICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgXSwKICAgICAgICAgICJuYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSIsCiAgICAgICAgICAidXJsIjogImh0dHBzOi8vMWVkdGVjaC5lZHUiLAogICAgICAgICAgInBob25lIjogIjEtMjIyLTMzMy00NDQ0IiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgcHJvdmlkZXMgb25saW5lIGRlZ3JlZSBwcm9ncmFtcy4iLAogICAgICAgICAgImVuZG9yc2VtZW50IjogWwogICAgICAgICAgICB7CiAgICAgICAgICAgICAgIkBjb250ZXh0IjogWwogICAgICAgICAgICAgICAgImh0dHBzOi8vd3d3LnczLm9yZy9ucy9jcmVkZW50aWFscy92MiIsCiAgICAgICAgICAgICAgICAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjMuanNvbiIKICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvZW5kb3JzZW1lbnRjcmVkZW50aWFsLzM3MzIiLAogICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgIlZlcmlmaWFibGVDcmVkZW50aWFsIiwKICAgICAgICAgICAgICAgICJFbmRvcnNlbWVudENyZWRlbnRpYWwiCiAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAibmFtZSI6ICJTREUgZW5kb3JzZW1lbnQiLAogICAgICAgICAgICAgICJpc3N1ZXIiOiB7CiAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSIsCiAgICAgICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAgICAgIlByb2ZpbGUiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgIm5hbWUiOiAiRXhhbXBsZSBBY2NyZWRpdGluZyBBZ2VuY3kiCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAidmFsaWRGcm9tIjogIjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwKICAgICAgICAgICAgICAidmFsaWRVbnRpbCI6ICIyMDIwLTAxLTAxVDAwOjAwOjAwWiIsCiAgICAgICAgICAgICAgImNyZWRlbnRpYWxTdWJqZWN0IjogewogICAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgICAgICJFbmRvcnNlbWVudFN1YmplY3QiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgImVuZG9yc2VtZW50Q29tbWVudCI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgaXMgaW4gZ29vZCBzdGFuZGluZyIKICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICJjcmVkZW50aWFsU2NoZW1hIjogWwogICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL3NjaGVtYS9qc29uL29iX3YzcDBfZW5kb3JzZW1lbnRjcmVkZW50aWFsX3NjaGVtYS5qc29uIiwKICAgICAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaEpzb25TY2hlbWFWYWxpZGF0b3IyMDE5IgogICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vYWNjcmVkaXRlci5lZHUvc2NoZW1hL2VuZG9yc2VtZW50Y3JlZGVudGlhbC5qc29uIiwKICAgICAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaEpzb25TY2hlbWFWYWxpZGF0b3IyMDE5IgogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgImNyZWRlbnRpYWxTdGF0dXMiOiB7CiAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9jcmVkZW50aWFscy8zNzMyL3Jldm9jYXRpb25zIiwKICAgICAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hSZXZvY2F0aW9uTGlzdCIKICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICJyZWZyZXNoU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMiIsCiAgICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoQ3JlZGVudGlhbFJlZnJlc2giCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAicHJvb2YiOiBbCiAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICJ0eXBlIjogIkRhdGFJbnRlZ3JpdHlQcm9vZiIsCiAgICAgICAgICAgICAgICAgICJjcnlwdG9zdWl0ZSI6ICJlZGRzYS1yZGYtMjAyMiIsCiAgICAgICAgICAgICAgICAgICJjcmVhdGVkIjogIjIwMjItMDUtMjZUMTg6MTc6MDhaIiwKICAgICAgICAgICAgICAgICAgInZlcmlmaWNhdGlvbk1ldGhvZCI6ICJodHRwczovL2FjY3JlZGl0ZXIuZWR1L2lzc3VlcnMvNTY1MDQ5I3p2UGtRaVVGZkpyZ25DUmh5UGtUU2tnckdYYm5MUjE1cEhINUhaVllOZE00VENBd1FIcUc3Zk1lTVBMdFlOUm5FZ29WMWFKZFI1RTYxZVd1NXNXUllndEEiLAogICAgICAgICAgICAgICAgICAicHJvb2ZQdXJwb3NlIjogImFzc2VydGlvbk1ldGhvZCIsCiAgICAgICAgICAgICAgICAgICJwcm9vZlZhbHVlIjogInp2UGtRaVVGZkpyZ25DUmh5UGtUU2tnckdYYm5MUjE1cEhINUhaVllOZE00VENBd1FIcUc3Zk1lTVBMdFlOUm5FZ29WMWFKZFI1RTYxZVd1NXNXUllndEEiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgXQogICAgICAgICAgICB9LAogICAgICAgICAgICB7CiAgICAgICAgICAgICAgIkBjb250ZXh0IjogWwogICAgICAgICAgICAgICAgImh0dHBzOi8vd3d3LnczLm9yZy9ucy9jcmVkZW50aWFscy92MiIsCiAgICAgICAgICAgICAgICAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjMuanNvbiIKICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvZW5kb3JzZW1lbnRjcmVkZW50aWFsLzM3MzMiLAogICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgIlZlcmlmaWFibGVDcmVkZW50aWFsIiwKICAgICAgICAgICAgICAgICJFbmRvcnNlbWVudENyZWRlbnRpYWwiCiAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAibmFtZSI6ICJTREUgZW5kb3JzZW1lbnQiLAogICAgICAgICAgICAgICJpc3N1ZXIiOiB7CiAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9zdGF0ZS5nb3YvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICJuYW1lIjogIlN0YXRlIERlcGFydG1lbnQgb2YgRWR1Y2F0aW9uIgogICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgInZhbGlkRnJvbSI6ICIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsCiAgICAgICAgICAgICAgInZhbGlkVW50aWwiOiAiMjAyMC0wMS0wMVQwMDowMDowMFoiLAogICAgICAgICAgICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgICAgICAgICAgICJpZCI6ICJodHRwczovLzFlZHRlY2guZWR1L2lzc3VlcnMvNTY1MDQ5IiwKICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAiRW5kb3JzZW1lbnRTdWJqZWN0IgogICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICJlbmRvcnNlbWVudENvbW1lbnQiOiAiMUVkVGVjaCBVbml2ZXJzaXR5IGlzIGluIGdvb2Qgc3RhbmRpbmciCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAiY3JlZGVudGlhbFNjaGVtYSI6IFsKICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9zY2hlbWEvanNvbi9vYl92M3AwX2VuZG9yc2VtZW50Y3JlZGVudGlhbF9zY2hlbWEuanNvbiIsCiAgICAgICAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hKc29uU2NoZW1hVmFsaWRhdG9yMjAxOSIKICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICJpZCI6ICJodHRwczovL3N0YXRlLmdvdi9zY2hlbWEvZW5kb3JzZW1lbnRjcmVkZW50aWFsLmpzb24iLAogICAgICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoSnNvblNjaGVtYVZhbGlkYXRvcjIwMTkiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAiY3JlZGVudGlhbFN0YXR1cyI6IHsKICAgICAgICAgICAgICAgICJpZCI6ICJodHRwczovL3N0YXRlLmdvdi9jcmVkZW50aWFscy8zNzMyL3Jldm9jYXRpb25zIiwKICAgICAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hSZXZvY2F0aW9uTGlzdCIKICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICJyZWZyZXNoU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICJpZCI6ICJodHRwOi8vc3RhdGUuZ292L2NyZWRlbnRpYWxzLzM3MzIiLAogICAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaENyZWRlbnRpYWxSZWZyZXNoIgogICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgInByb29mIjogWwogICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICAgICAgICAgICAiY3J5cHRvc3VpdGUiOiAiZWRkc2EtcmRmLTIwMjIiLAogICAgICAgICAgICAgICAgICAiY3JlYXRlZCI6ICIyMDIyLTA1LTI2VDE4OjI1OjU5WiIsCiAgICAgICAgICAgICAgICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSN6NWJEbm1TZ0Rjelh3Wkd5YTZaanhLYXhrZEt4enNDTWlWU3NnRVZXeG5hV0s3WnFiS256Y0NkN21VS0U5RFFhQUwyUU1YUDVBcXVQZVc2VzJDV3JaN2pOQyIsCiAgICAgICAgICAgICAgICAgICJwcm9vZlB1cnBvc2UiOiAiYXNzZXJ0aW9uTWV0aG9kIiwKICAgICAgICAgICAgICAgICAgInByb29mVmFsdWUiOiAiejViRG5tU2dEY3pYd1pHeWE2Wmp4S2F4a2RLeHpzQ01pVlNzZ0VWV3huYVdLN1pxYktuemNDZDdtVUtFOURRYUFMMlFNWFA1QXF1UGVXNlcyQ1dyWjdqTkMiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgXQogICAgICAgICAgICB9CiAgICAgICAgICBdLAogICAgICAgICAgImltYWdlIjogewogICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9sb2dvLnBuZyIsCiAgICAgICAgICAgICJ0eXBlIjogIkltYWdlIiwKICAgICAgICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IGxvZ28iCiAgICAgICAgICB9LAogICAgICAgICAgImVtYWlsIjogInJlZ2lzdHJhckAxZWR0ZWNoLmVkdSIsCiAgICAgICAgICAiYWRkcmVzcyI6IHsKICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgIkFkZHJlc3MiCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJhZGRyZXNzQ291bnRyeSI6ICJVU0EiLAogICAgICAgICAgICAiYWRkcmVzc0NvdW50cnlDb2RlIjogIlVTIiwKICAgICAgICAgICAgImFkZHJlc3NSZWdpb24iOiAiVFgiLAogICAgICAgICAgICAiYWRkcmVzc0xvY2FsaXR5IjogIkF1c3RpbiIsCiAgICAgICAgICAgICJzdHJlZXRBZGRyZXNzIjogIjEyMyBGaXJzdCBTdCIsCiAgICAgICAgICAgICJwb3N0T2ZmaWNlQm94TnVtYmVyIjogIjEiLAogICAgICAgICAgICAicG9zdGFsQ29kZSI6ICIxMjM0NSIsCiAgICAgICAgICAgICJnZW8iOiB7CiAgICAgICAgICAgICAgInR5cGUiOiAiR2VvQ29vcmRpbmF0ZXMiLAogICAgICAgICAgICAgICJsYXRpdHVkZSI6IDEsCiAgICAgICAgICAgICAgImxvbmdpdHVkZSI6IDEKICAgICAgICAgICAgfQogICAgICAgICAgfSwKICAgICAgICAgICJvdGhlcklkZW50aWZpZXIiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAidHlwZSI6ICJJZGVudGlmaWVyRW50cnkiLAogICAgICAgICAgICAgICJpZGVudGlmaWVyIjogIjEyMzQ1IiwKICAgICAgICAgICAgICAiaWRlbnRpZmllclR5cGUiOiAic291cmNlZElkIgogICAgICAgICAgICB9LAogICAgICAgICAgICB7CiAgICAgICAgICAgICAgInR5cGUiOiAiSWRlbnRpZmllckVudHJ5IiwKICAgICAgICAgICAgICAiaWRlbnRpZmllciI6ICI2Nzg5MCIsCiAgICAgICAgICAgICAgImlkZW50aWZpZXJUeXBlIjogIm5hdGlvbmFsSWRlbnRpdHlOdW1iZXIiCiAgICAgICAgICAgIH0KICAgICAgICAgIF0sCiAgICAgICAgICAib2ZmaWNpYWwiOiAiSG9yYWNlIE1hbm4iLAogICAgICAgICAgInBhcmVudE9yZyI6IHsKICAgICAgICAgICAgImlkIjogImRpZDpleGFtcGxlOjEyMzQ1Njc4OSIsCiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgICBdLAogICAgICAgICAgICAibmFtZSI6ICJVbml2ZXJzYWwgVW5pdmVyc2l0aWVzIgogICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgImNyZWRpdHNBdmFpbGFibGUiOiAzNiwKICAgICAgICAiY3JpdGVyaWEiOiB7CiAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9hY2hpZXZlbWVudHMvZGVncmVlIiwKICAgICAgICAgICJuYXJyYXRpdmUiOiAiIyBEZWdyZWUgUmVxdWlyZW1lbnRzXG5TdHVkZW50cyBtdXN0IGNvbXBsZXRlLi4uIgogICAgICAgIH0sCiAgICAgICAgImRlc2NyaXB0aW9uIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUgRGVzY3JpcHRpb24iLAogICAgICAgICJlbmRvcnNlbWVudCI6IFsKICAgICAgICAgIHsKICAgICAgICAgICAgIkBjb250ZXh0IjogWwogICAgICAgICAgICAgICJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLAogICAgICAgICAgICAgICJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL29iL3YzcDAvY29udGV4dC0zLjAuMy5qc29uIgogICAgICAgICAgICBdLAogICAgICAgICAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2VuZG9yc2VtZW50Y3JlZGVudGlhbC8zNzM0IiwKICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgIlZlcmlmaWFibGVDcmVkZW50aWFsIiwKICAgICAgICAgICAgICAiRW5kb3JzZW1lbnRDcmVkZW50aWFsIgogICAgICAgICAgICBdLAogICAgICAgICAgICAibmFtZSI6ICJFQUEgZW5kb3JzZW1lbnQiLAogICAgICAgICAgICAiaXNzdWVyIjogewogICAgICAgICAgICAgICJpZCI6ICJodHRwczovL2FjY3JlZGl0ZXIuZWR1L2lzc3VlcnMvNTY1MDQ5IiwKICAgICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgIm5hbWUiOiAiRXhhbXBsZSBBY2NyZWRpdGluZyBBZ2VuY3kiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJ2YWxpZEZyb20iOiAiMjAxMC0wMS0wMVQwMDowMDowMFoiLAogICAgICAgICAgICAidmFsaWRVbnRpbCI6ICIyMDIwLTAxLTAxVDAwOjAwOjAwWiIsCiAgICAgICAgICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9pc3N1ZXJzLzU2NTA0OSIsCiAgICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgICAiRW5kb3JzZW1lbnRTdWJqZWN0IgogICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgImVuZG9yc2VtZW50Q29tbWVudCI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgaXMgaW4gZ29vZCBzdGFuZGluZyIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgImNyZWRlbnRpYWxTY2hlbWEiOiBbCiAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9zY2hlbWEvanNvbi9vYl92M3AwX2VuZG9yc2VtZW50Y3JlZGVudGlhbF9zY2hlbWEuanNvbiIsCiAgICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoSnNvblNjaGVtYVZhbGlkYXRvcjIwMTkiCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9zY2hlbWEvZW5kb3JzZW1lbnRjcmVkZW50aWFsLmpzb24iLAogICAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaEpzb25TY2hlbWFWYWxpZGF0b3IyMDE5IgogICAgICAgICAgICAgIH0KICAgICAgICAgICAgXSwKICAgICAgICAgICAgImNyZWRlbnRpYWxTdGF0dXMiOiB7CiAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMi9yZXZvY2F0aW9ucyIsCiAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaFJldm9jYXRpb25MaXN0IgogICAgICAgICAgICB9LAogICAgICAgICAgICAicmVmcmVzaFNlcnZpY2UiOiB7CiAgICAgICAgICAgICAgImlkIjogImh0dHA6Ly8xZWR0ZWNoLmVkdS9jcmVkZW50aWFscy8zNzMyIiwKICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoQ3JlZGVudGlhbFJlZnJlc2giCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJwcm9vZiI6IFsKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICAgICAgICAgImNyeXB0b3N1aXRlIjogImVkZHNhLXJkZi0yMDIyIiwKICAgICAgICAgICAgICAgICJjcmVhdGVkIjogIjIwMjItMDUtMjZUMTg6MTc6MDhaIiwKICAgICAgICAgICAgICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSN6dlBrUWlVRmZKcmduQ1JoeVBrVFNrZ3JHWGJuTFIxNXBISDVIWlZZTmRNNFRDQXdRSHFHN2ZNZU1QTHRZTlJuRWdvVjFhSmRSNUU2MWVXdTVzV1JZZ3RBIiwKICAgICAgICAgICAgICAgICJwcm9vZlB1cnBvc2UiOiAiYXNzZXJ0aW9uTWV0aG9kIiwKICAgICAgICAgICAgICAgICJwcm9vZlZhbHVlIjogInp2UGtRaVVGZkpyZ25DUmh5UGtUU2tnckdYYm5MUjE1cEhINUhaVllOZE00VENBd1FIcUc3Zk1lTVBMdFlOUm5FZ29WMWFKZFI1RTYxZVd1NXNXUllndEEiCiAgICAgICAgICAgICAgfQogICAgICAgICAgICBdCiAgICAgICAgICB9CiAgICAgICAgXSwKICAgICAgICAiZmllbGRPZlN0dWR5IjogIlJlc2VhcmNoIiwKICAgICAgICAiaHVtYW5Db2RlIjogIlIxIiwKICAgICAgICAiaW1hZ2UiOiB7CiAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2hhbXJ0L2NyZWRlbnRpYWwtY29udmVydGVyL3JlZnMvaGVhZHMvaW1hZ2UvdGVzdC9lZHViYWRnZXNfMTAweDEwMC5wbmciLAogICAgICAgICAgInR5cGUiOiAiSW1hZ2UiLAogICAgICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IERlZ3JlZSIKICAgICAgICB9LAogICAgICAgICJuYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUiLAogICAgICAgICJvdGhlcklkZW50aWZpZXIiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0eXBlIjogIklkZW50aWZpZXJFbnRyeSIsCiAgICAgICAgICAgICJpZGVudGlmaWVyIjogImFiZGUiLAogICAgICAgICAgICAiaWRlbnRpZmllclR5cGUiOiAiaWRlbnRpZmllciIKICAgICAgICAgIH0KICAgICAgICBdLAogICAgICAgICJyZXN1bHREZXNjcmlwdGlvbiI6IFsKICAgICAgICAgIHsKICAgICAgICAgICAgImlkIjogInVybjp1dWlkOmY2YWIyNGNkLTg2ZTgtNGVhZi1iOGM2LWRlZDc0ZThmZDQxYyIsCiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJSZXN1bHREZXNjcmlwdGlvbiIKICAgICAgICAgICAgXSwKICAgICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgICAidGFyZ2V0Q29kZSI6ICJwcm9qZWN0IiwKICAgICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAgICJ0YXJnZXROYW1lIjogIkZpbmFsIFByb2plY3QiLAogICAgICAgICAgICAgICAgInRhcmdldEZyYW1ld29yayI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgUHJvZ3JhbSBhbmQgQ291cnNlIENhdGFsb2ciLAogICAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZJdGVtIiwKICAgICAgICAgICAgICAgICJ0YXJnZXRVcmwiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9jYXRhbG9nL2RlZ3JlZS9wcm9qZWN0IgogICAgICAgICAgICAgIH0KICAgICAgICAgICAgXSwKICAgICAgICAgICAgImFsbG93ZWRWYWx1ZSI6IFsKICAgICAgICAgICAgICAiRCIsCiAgICAgICAgICAgICAgIkMiLAogICAgICAgICAgICAgICJCIiwKICAgICAgICAgICAgICAiQSIKICAgICAgICAgICAgXSwKICAgICAgICAgICAgIm5hbWUiOiAiRmluYWwgUHJvamVjdCBHcmFkZSIsCiAgICAgICAgICAgICJyZXF1aXJlZFZhbHVlIjogIkMiLAogICAgICAgICAgICAicmVzdWx0VHlwZSI6ICJMZXR0ZXJHcmFkZSIKICAgICAgICAgIH0sCiAgICAgICAgICB7CiAgICAgICAgICAgICJpZCI6ICJ1cm46dXVpZDphNzBkZGM2YS00YzRhLTRiZDgtODI3Ny1jYjk3Yzc5ZjQwYzUiLAogICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAiUmVzdWx0RGVzY3JpcHRpb24iCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJhbGlnbm1lbnQiOiBbCiAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgICAgICJBbGlnbm1lbnQiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgInRhcmdldENvZGUiOiAicHJvamVjdCIsCiAgICAgICAgICAgICAgICAidGFyZ2V0RGVzY3JpcHRpb24iOiAiUHJvamVjdCBkZXNjcmlwdGlvbiIsCiAgICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAgICJ0YXJnZXRGcmFtZXdvcmsiOiAiMUVkVGVjaCBVbml2ZXJzaXR5IFByb2dyYW0gYW5kIENvdXJzZSBDYXRhbG9nIiwKICAgICAgICAgICAgICAgICJ0YXJnZXRUeXBlIjogIkNGSXRlbSIsCiAgICAgICAgICAgICAgICAidGFyZ2V0VXJsIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY2F0YWxvZy9kZWdyZWUvcHJvamVjdCIKICAgICAgICAgICAgICB9CiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJhbGxvd2VkVmFsdWUiOiBbCiAgICAgICAgICAgICAgIkQiLAogICAgICAgICAgICAgICJDIiwKICAgICAgICAgICAgICAiQiIsCiAgICAgICAgICAgICAgIkEiCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJuYW1lIjogIkZpbmFsIFByb2plY3QgR3JhZGUiLAogICAgICAgICAgICAicmVxdWlyZWRMZXZlbCI6ICJ1cm46dXVpZDpkMDVhMDg2Ny1kMGFkLTRiMDMtYmRiNS0yOGZiNWQyYWFiN2EiLAogICAgICAgICAgICAicmVzdWx0VHlwZSI6ICJSdWJyaWNDcml0ZXJpb25MZXZlbCIsCiAgICAgICAgICAgICJydWJyaWNDcml0ZXJpb25MZXZlbCI6IFsKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAiaWQiOiAidXJuOnV1aWQ6ZDA1YTA4NjctZDBhZC00YjAzLWJkYjUtMjhmYjVkMmFhYjdhIiwKICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAiUnVicmljQ3JpdGVyaW9uTGV2ZWwiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogInByb2plY3QiLAogICAgICAgICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZSdWJyaWNDcml0ZXJpb25MZXZlbCIsCiAgICAgICAgICAgICAgICAgICAgInRhcmdldFVybCI6ICJodHRwczovLzFlZHRlY2guZWR1L2NhdGFsb2cvZGVncmVlL3Byb2plY3QvcnVicmljL2xldmVscy9tYXN0ZXJlZCIKICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJUaGUgYXV0aG9yIGRlbW9uc3RyYXRlZC4uLiIsCiAgICAgICAgICAgICAgICAibGV2ZWwiOiAiTWFzdGVyZWQiLAogICAgICAgICAgICAgICAgIm5hbWUiOiAiTWFzdGVyeSIsCiAgICAgICAgICAgICAgICAicG9pbnRzIjogIjQiCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAiaWQiOiAidXJuOnV1aWQ6NmI4NGI0MjktMzFlZS00ZGFjLTlkMjAtZTVjNTU4ODFmODBlIiwKICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAiUnVicmljQ3JpdGVyaW9uTGV2ZWwiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogInByb2plY3QiLAogICAgICAgICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZSdWJyaWNDcml0ZXJpb25MZXZlbCIsCiAgICAgICAgICAgICAgICAgICAgInRhcmdldFVybCI6ICJodHRwczovLzFlZHRlY2guZWR1L2NhdGFsb2cvZGVncmVlL3Byb2plY3QvcnVicmljL2xldmVscy9iYXNpYyIKICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJUaGUgYXV0aG9yIGRlbW9uc3RyYXRlZC4uLiIsCiAgICAgICAgICAgICAgICAibGV2ZWwiOiAiQmFzaWMiLAogICAgICAgICAgICAgICAgIm5hbWUiOiAiQmFzaWMiLAogICAgICAgICAgICAgICAgInBvaW50cyI6ICI0IgogICAgICAgICAgICAgIH0KICAgICAgICAgICAgXQogICAgICAgICAgfSwKICAgICAgICAgIHsKICAgICAgICAgICAgImlkIjogInVybjp1dWlkOmIwN2MwMzg3LWYyZDYtNGI2NS1hM2Y0LWY0ZTQzMDJlYThmNyIsCiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJSZXN1bHREZXNjcmlwdGlvbiIKICAgICAgICAgICAgXSwKICAgICAgICAgICAgIm5hbWUiOiAiUHJvamVjdCBTdGF0dXMiLAogICAgICAgICAgICAicmVzdWx0VHlwZSI6ICJTdGF0dXMiCiAgICAgICAgICB9CiAgICAgICAgXSwKICAgICAgICAic3BlY2lhbGl6YXRpb24iOiAiQ29tcHV0ZXIgU2NpZW5jZSBSZXNlYXJjaCIsCiAgICAgICAgInRhZyI6IFsKICAgICAgICAgICJyZXNlYXJjaCIsCiAgICAgICAgICAiY29tcHV0ZXIgc2NpZW5jZSIKICAgICAgICBdCiAgICAgIH0sCiAgICAgICJpbWFnZSI6IHsKICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9jcmVkZW50aWFscy8zNzMyL2ltYWdlIiwKICAgICAgICAidHlwZSI6ICJJbWFnZSIsCiAgICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IERlZ3JlZSBmb3IgRXhhbXBsZSBTdHVkZW50IgogICAgICB9LAogICAgICAibmFycmF0aXZlIjogIlRoZXJlIGlzIGEgZmluYWwgcHJvamVjdCByZXBvcnQgYW5kIHNvdXJjZSBjb2RlIGV2aWRlbmNlLiIsCiAgICAgICJyZXN1bHQiOiBbCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICJSZXN1bHQiCiAgICAgICAgICBdLAogICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgewogICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogInByb2plY3QiLAogICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZJdGVtIiwKICAgICAgICAgICAgICAidGFyZ2V0VXJsIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY2F0YWxvZy9kZWdyZWUvcHJvamVjdC9yZXN1bHQvMSIKICAgICAgICAgICAgfQogICAgICAgICAgXSwKICAgICAgICAgICJyZXN1bHREZXNjcmlwdGlvbiI6ICJ1cm46dXVpZDpmNmFiMjRjZC04NmU4LTRlYWYtYjhjNi1kZWQ3NGU4ZmQ0MWMiLAogICAgICAgICAgInZhbHVlIjogIkEiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgIlJlc3VsdCIKICAgICAgICAgIF0sCiAgICAgICAgICAiYWNoaWV2ZWRMZXZlbCI6ICJ1cm46dXVpZDpkMDVhMDg2Ny1kMGFkLTRiMDMtYmRiNS0yOGZiNWQyYWFiN2EiLAogICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgewogICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogInByb2plY3QiLAogICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZJdGVtIiwKICAgICAgICAgICAgICAidGFyZ2V0VXJsIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY2F0YWxvZy9kZWdyZWUvcHJvamVjdC9yZXN1bHQvMSIKICAgICAgICAgICAgfQogICAgICAgICAgXSwKICAgICAgICAgICJyZXN1bHREZXNjcmlwdGlvbiI6ICJ1cm46dXVpZDpmNmFiMjRjZC04NmU4LTRlYWYtYjhjNi1kZWQ3NGU4ZmQ0MWMiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgIlJlc3VsdCIKICAgICAgICAgIF0sCiAgICAgICAgICAicmVzdWx0RGVzY3JpcHRpb24iOiAidXJuOnV1aWQ6ZjZhYjI0Y2QtODZlOC00ZWFmLWI4YzYtZGVkNzRlOGZkNDFjIiwKICAgICAgICAgICJzdGF0dXMiOiAiQ29tcGxldGVkIgogICAgICAgIH0KICAgICAgXQogICAgfSwKICAgICJlbmRvcnNlbWVudCI6IFsKICAgICAgewogICAgICAgICJAY29udGV4dCI6IFsKICAgICAgICAgICJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLAogICAgICAgICAgImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9jb250ZXh0LTMuMC4zLmpzb24iCiAgICAgICAgXSwKICAgICAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2VuZG9yc2VtZW50Y3JlZGVudGlhbC8zNzM1IiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsCiAgICAgICAgICAiRW5kb3JzZW1lbnRDcmVkZW50aWFsIgogICAgICAgIF0sCiAgICAgICAgIm5hbWUiOiAiRUFBIGVuZG9yc2VtZW50IiwKICAgICAgICAiaXNzdWVyIjogewogICAgICAgICAgImlkIjogImh0dHBzOi8vYWNjcmVkaXRlci5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgXSwKICAgICAgICAgICJuYW1lIjogIkV4YW1wbGUgQWNjcmVkaXRpbmcgQWdlbmN5IgogICAgICAgIH0sCiAgICAgICAgInZhbGlkRnJvbSI6ICIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsCiAgICAgICAgInZhbGlkVW50aWwiOiAiMjAyMC0wMS0wMVQwMDowMDowMFoiLAogICAgICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgICAgICJpZCI6ICJodHRwczovLzFlZHRlY2guZWR1L2lzc3VlcnMvNTY1MDQ5IiwKICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAiRW5kb3JzZW1lbnRTdWJqZWN0IgogICAgICAgICAgXSwKICAgICAgICAgICJlbmRvcnNlbWVudENvbW1lbnQiOiAiMUVkVGVjaCBVbml2ZXJzaXR5IGlzIGluIGdvb2Qgc3RhbmRpbmciCiAgICAgICAgfSwKICAgICAgICAiY3JlZGVudGlhbFNjaGVtYSI6IFsKICAgICAgICAgIHsKICAgICAgICAgICAgImlkIjogImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9zY2hlbWEvanNvbi9vYl92M3AwX2VuZG9yc2VtZW50Y3JlZGVudGlhbF9zY2hlbWEuanNvbiIsCiAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hKc29uU2NoZW1hVmFsaWRhdG9yMjAxOSIKICAgICAgICAgIH0sCiAgICAgICAgICB7CiAgICAgICAgICAgICJpZCI6ICJodHRwczovL2FjY3JlZGl0ZXIuZWR1L3NjaGVtYS9lbmRvcnNlbWVudGNyZWRlbnRpYWwuanNvbiIsCiAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hKc29uU2NoZW1hVmFsaWRhdG9yMjAxOSIKICAgICAgICAgIH0KICAgICAgICBdLAogICAgICAgICJjcmVkZW50aWFsU3RhdHVzIjogewogICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMi9yZXZvY2F0aW9ucyIsCiAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoUmV2b2NhdGlvbkxpc3QiCiAgICAgICAgfSwKICAgICAgICAicmVmcmVzaFNlcnZpY2UiOiB7CiAgICAgICAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2NyZWRlbnRpYWxzLzM3MzIiLAogICAgICAgICAgInR5cGUiOiAiMUVkVGVjaENyZWRlbnRpYWxSZWZyZXNoIgogICAgICAgIH0sCiAgICAgICAgInByb29mIjogWwogICAgICAgICAgewogICAgICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICAgICAiY3J5cHRvc3VpdGUiOiAiZWRkc2EtcmRmLTIwMjIiLAogICAgICAgICAgICAiY3JlYXRlZCI6ICIyMDIyLTA1LTI2VDE4OjE3OjA4WiIsCiAgICAgICAgICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSN6dlBrUWlVRmZKcmduQ1JoeVBrVFNrZ3JHWGJuTFIxNXBISDVIWlZZTmRNNFRDQXdRSHFHN2ZNZU1QTHRZTlJuRWdvVjFhSmRSNUU2MWVXdTVzV1JZZ3RBIiwKICAgICAgICAgICAgInByb29mUHVycG9zZSI6ICJhc3NlcnRpb25NZXRob2QiLAogICAgICAgICAgICAicHJvb2ZWYWx1ZSI6ICJ6dlBrUWlVRmZKcmduQ1JoeVBrVFNrZ3JHWGJuTFIxNXBISDVIWlZZTmRNNFRDQXdRSHFHN2ZNZU1QTHRZTlJuRWdvVjFhSmRSNUU2MWVXdTVzV1JZZ3RBIgogICAgICAgICAgfQogICAgICAgIF0KICAgICAgfQogICAgXSwKICAgICJldmlkZW5jZSI6IFsKICAgICAgewogICAgICAgICJpZCI6ICJodHRwczovLzFlZHRlY2guZWR1L2NyZWRlbnRpYWxzLzM3MzIvZXZpZGVuY2UvMSIsCiAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAiRXZpZGVuY2UiCiAgICAgICAgXSwKICAgICAgICAibmFycmF0aXZlIjogIiMgRmluYWwgUHJvamVjdCBSZXBvcnQgXG4gVGhpcyBwcm9qZWN0IHdhcyAuLi4iLAogICAgICAgICJuYW1lIjogIkZpbmFsIFByb2plY3QgUmVwb3J0IiwKICAgICAgICAiZGVzY3JpcHRpb24iOiAiVGhpcyBpcyB0aGUgZmluYWwgcHJvamVjdCByZXBvcnQuIiwKICAgICAgICAiZ2VucmUiOiAiUmVzZWFyY2giLAogICAgICAgICJhdWRpZW5jZSI6ICJEZXBhcnRtZW50IgogICAgICB9LAogICAgICB7CiAgICAgICAgImlkIjogImh0dHBzOi8vZ2l0aHViLmNvbS9zb21lYm9keS9wcm9qZWN0IiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJFdmlkZW5jZSIKICAgICAgICBdLAogICAgICAgICJuYW1lIjogIkZpbmFsIFByb2plY3QgQ29kZSIsCiAgICAgICAgImRlc2NyaXB0aW9uIjogIlRoaXMgaXMgdGhlIHNvdXJjZSBjb2RlIGZvciB0aGUgZmluYWwgcHJvamVjdCBhcHAuIiwKICAgICAgICAiZ2VucmUiOiAiUmVzZWFyY2giLAogICAgICAgICJhdWRpZW5jZSI6ICJEZXBhcnRtZW50IgogICAgICB9CiAgICBdLAogICAgImlzc3VlciI6IHsKICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAidHlwZSI6IFsKICAgICAgICAiUHJvZmlsZSIKICAgICAgXSwKICAgICAgIm5hbWUiOiAiMUVkVGVjaCBVbml2ZXJzaXR5IiwKICAgICAgInVybCI6ICJodHRwczovLzFlZHRlY2guZWR1IiwKICAgICAgInBob25lIjogIjEtMjIyLTMzMy00NDQ0IiwKICAgICAgImRlc2NyaXB0aW9uIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBwcm92aWRlcyBvbmxpbmUgZGVncmVlIHByb2dyYW1zLiIsCiAgICAgICJlbmRvcnNlbWVudCI6IFsKICAgICAgICB7CiAgICAgICAgICAiQGNvbnRleHQiOiBbCiAgICAgICAgICAgICJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLAogICAgICAgICAgICAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjMuanNvbiIKICAgICAgICAgIF0sCiAgICAgICAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2VuZG9yc2VtZW50Y3JlZGVudGlhbC8zNzM2IiwKICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLAogICAgICAgICAgICAiRW5kb3JzZW1lbnRDcmVkZW50aWFsIgogICAgICAgICAgXSwKICAgICAgICAgICJuYW1lIjogIkVBQSBlbmRvcnNlbWVudCIsCiAgICAgICAgICAiaXNzdWVyIjogewogICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSIsCiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgICBdLAogICAgICAgICAgICAibmFtZSI6ICJFeGFtcGxlIEFjY3JlZGl0aW5nIEFnZW5jeSIKICAgICAgICAgIH0sCiAgICAgICAgICAidmFsaWRGcm9tIjogIjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwKICAgICAgICAgICJ2YWxpZFVudGlsIjogIjIwMjAtMDEtMDFUMDA6MDA6MDBaIiwKICAgICAgICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAiRW5kb3JzZW1lbnRTdWJqZWN0IgogICAgICAgICAgICBdLAogICAgICAgICAgICAiZW5kb3JzZW1lbnRDb21tZW50IjogIjFFZFRlY2ggVW5pdmVyc2l0eSBpcyBpbiBnb29kIHN0YW5kaW5nIgogICAgICAgICAgfSwKICAgICAgICAgICJjcmVkZW50aWFsU2NoZW1hIjogWwogICAgICAgICAgICB7CiAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9zY2hlbWEvanNvbi9vYl92M3AwX2VuZG9yc2VtZW50Y3JlZGVudGlhbF9zY2hlbWEuanNvbiIsCiAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaEpzb25TY2hlbWFWYWxpZGF0b3IyMDE5IgogICAgICAgICAgICB9LAogICAgICAgICAgICB7CiAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vYWNjcmVkaXRlci5lZHUvc2NoZW1hL2VuZG9yc2VtZW50Y3JlZGVudGlhbC5qc29uIiwKICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoSnNvblNjaGVtYVZhbGlkYXRvcjIwMTkiCiAgICAgICAgICAgIH0KICAgICAgICAgIF0sCiAgICAgICAgICAiY3JlZGVudGlhbFN0YXR1cyI6IHsKICAgICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMi9yZXZvY2F0aW9ucyIsCiAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hSZXZvY2F0aW9uTGlzdCIKICAgICAgICAgIH0sCiAgICAgICAgICAicmVmcmVzaFNlcnZpY2UiOiB7CiAgICAgICAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMiIsCiAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hDcmVkZW50aWFsUmVmcmVzaCIKICAgICAgICAgIH0sCiAgICAgICAgICAicHJvb2YiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICAgICAgICJjcnlwdG9zdWl0ZSI6ICJlZGRzYS1yZGYtMjAyMiIsCiAgICAgICAgICAgICAgImNyZWF0ZWQiOiAiMjAyMi0wNS0yNlQxODoxNzowOFoiLAogICAgICAgICAgICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSN6dlBrUWlVRmZKcmduQ1JoeVBrVFNrZ3JHWGJuTFIxNXBISDVIWlZZTmRNNFRDQXdRSHFHN2ZNZU1QTHRZTlJuRWdvVjFhSmRSNUU2MWVXdTVzV1JZZ3RBIiwKICAgICAgICAgICAgICAicHJvb2ZQdXJwb3NlIjogImFzc2VydGlvbk1ldGhvZCIsCiAgICAgICAgICAgICAgInByb29mVmFsdWUiOiAienZQa1FpVUZmSnJnbkNSaHlQa1RTa2dyR1hibkxSMTVwSEg1SFpWWU5kTTRUQ0F3UUhxRzdmTWVNUEx0WU5SbkVnb1YxYUpkUjVFNjFlV3U1c1dSWWd0QSIKICAgICAgICAgICAgfQogICAgICAgICAgXQogICAgICAgIH0KICAgICAgXSwKICAgICAgImltYWdlIjogewogICAgICAgICJpZCI6ICJodHRwczovLzFlZHRlY2guZWR1L2xvZ28ucG5nIiwKICAgICAgICAidHlwZSI6ICJJbWFnZSIsCiAgICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IGxvZ28iCiAgICAgIH0sCiAgICAgICJlbWFpbCI6ICJyZWdpc3RyYXJAMWVkdGVjaC5lZHUiLAogICAgICAiYWRkcmVzcyI6IHsKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJBZGRyZXNzIgogICAgICAgIF0sCiAgICAgICAgImFkZHJlc3NDb3VudHJ5IjogIlVTQSIsCiAgICAgICAgImFkZHJlc3NDb3VudHJ5Q29kZSI6ICJVUyIsCiAgICAgICAgImFkZHJlc3NSZWdpb24iOiAiVFgiLAogICAgICAgICJhZGRyZXNzTG9jYWxpdHkiOiAiQXVzdGluIiwKICAgICAgICAic3RyZWV0QWRkcmVzcyI6ICIxMjMgRmlyc3QgU3QiLAogICAgICAgICJwb3N0T2ZmaWNlQm94TnVtYmVyIjogIjEiLAogICAgICAgICJwb3N0YWxDb2RlIjogIjEyMzQ1IiwKICAgICAgICAiZ2VvIjogewogICAgICAgICAgInR5cGUiOiAiR2VvQ29vcmRpbmF0ZXMiLAogICAgICAgICAgImxhdGl0dWRlIjogMSwKICAgICAgICAgICJsb25naXR1ZGUiOiAxCiAgICAgICAgfQogICAgICB9LAogICAgICAib3RoZXJJZGVudGlmaWVyIjogWwogICAgICAgIHsKICAgICAgICAgICJ0eXBlIjogIklkZW50aWZpZXJFbnRyeSIsCiAgICAgICAgICAiaWRlbnRpZmllciI6ICIxMjM0NSIsCiAgICAgICAgICAiaWRlbnRpZmllclR5cGUiOiAic291cmNlZElkIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiAiSWRlbnRpZmllckVudHJ5IiwKICAgICAgICAgICJpZGVudGlmaWVyIjogIjY3ODkwIiwKICAgICAgICAgICJpZGVudGlmaWVyVHlwZSI6ICJuYXRpb25hbElkZW50aXR5TnVtYmVyIgogICAgICAgIH0KICAgICAgXSwKICAgICAgIm9mZmljaWFsIjogIkhvcmFjZSBNYW5uIiwKICAgICAgInBhcmVudE9yZyI6IHsKICAgICAgICAiaWQiOiAiZGlkOmV4YW1wbGU6MTIzNDU2Nzg5IiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJQcm9maWxlIgogICAgICAgIF0sCiAgICAgICAgIm5hbWUiOiAiVW5pdmVyc2FsIFVuaXZlcnNpdGllcyIKICAgICAgfQogICAgfSwKICAgICJ2YWxpZEZyb20iOiAiMjAxMC0wMS0wMVQwMDowMDowMFoiLAogICAgInZhbGlkVW50aWwiOiAiMjAzMC0wMS0wMVQwMDowMDowMFoiLAogICAgImNyZWRlbnRpYWxTY2hlbWEiOiBbCiAgICAgIHsKICAgICAgICAiaWQiOiAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL3NjaGVtYS9qc29uL29iX3YzcDBfYWNoaWV2ZW1lbnRjcmVkZW50aWFsX3NjaGVtYS5qc29uIiwKICAgICAgICAidHlwZSI6ICIxRWRUZWNoSnNvblNjaGVtYVZhbGlkYXRvcjIwMTkiCiAgICAgIH0KICAgIF0sCiAgICAiY3JlZGVudGlhbFN0YXR1cyI6IHsKICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMi9yZXZvY2F0aW9ucyIsCiAgICAgICJ0eXBlIjogIjFFZFRlY2hSZXZvY2F0aW9uTGlzdCIKICAgIH0sCiAgICAicmVmcmVzaFNlcnZpY2UiOiB7CiAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMiIsCiAgICAgICJ0eXBlIjogIjFFZFRlY2hDcmVkZW50aWFsUmVmcmVzaCIKICAgIH0sCiAgICAicHJvb2YiOiBbCiAgICAgIHsKICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICJjcmVhdGVkIjogIjIwMjQtMDUtMzFUMTQ6MDU6MjVaIiwKICAgICAgICAidmVyaWZpY2F0aW9uTWV0aG9kIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkjejZNa3BoVTZRbW9qQzZHZFVCTll5cGduR2FpTDJUTGlzTE14cEUxb1pjbUtnN0FkIiwKICAgICAgICAiY3J5cHRvc3VpdGUiOiAiZWRkc2EtcmRmYy0yMDIyIiwKICAgICAgICAicHJvb2ZQdXJwb3NlIjogImFzc2VydGlvbk1ldGhvZCIsCiAgICAgICAgInByb29mVmFsdWUiOiAiejVBNFpYTEphNGRVQXJUbXBkUDl2bnJZaWpNTENUMXRSOUtXYUZtTFQyUGVRcDNnU25HQTl3clJKcXJKNVo4WW5wVkR4WlFXUkdqaldOYmoyUEtESmU3ZHQiCiAgICAgIH0KICAgIF0KfQo=" + "Content" : "ewogICAgIkBjb250ZXh0IjogWwogICAgICAiaHR0cHM6Ly93d3cudzMub3JnL25zL2NyZWRlbnRpYWxzL3YyIiwKICAgICAgImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9jb250ZXh0LTMuMC4zLmpzb24iLAogICAgICAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2V4dGVuc2lvbnMuanNvbiIKICAgIF0sCiAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2NyZWRlbnRpYWxzLzM3MzIiLAogICAgInR5cGUiOiBbCiAgICAgICJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsCiAgICAgICJPcGVuQmFkZ2VDcmVkZW50aWFsIgogICAgXSwKICAgICJuYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUgZm9yIEV4YW1wbGUgU3R1ZGVudCIsCiAgICAiZGVzY3JpcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IERlZ3JlZSBEZXNjcmlwdGlvbiIsCiAgICAiaW1hZ2UiOiB7CiAgICAgICJpZCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vaGFtcnQvY3JlZGVudGlhbC1jb252ZXJ0ZXIvcmVmcy9oZWFkcy9pbWFnZS90ZXN0L2VkdWJhZGdlc18xMDB4MTAwLnBuZyIsCiAgICAgICJ0eXBlIjogIkltYWdlIiwKICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IERlZ3JlZSBmb3IgRXhhbXBsZSBTdHVkZW50IgogICAgfSwKICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgImlkIjogImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsCiAgICAgICJ0eXBlIjogWwogICAgICAgICJBY2hpZXZlbWVudFN1YmplY3QiCiAgICAgIF0sCiAgICAgICJhY3Rpdml0eUVuZERhdGUiOiAiMjAxMC0wMS0wMlQwMDowMDowMFoiLAogICAgICAiYWN0aXZpdHlTdGFydERhdGUiOiAiMjAxMC0wMS0wMVQwMDowMDowMFoiLAogICAgICAiY3JlZGl0c0Vhcm5lZCI6IDQyLAogICAgICAibGljZW5zZU51bWJlciI6ICJBLTkzMjAwNDEiLAogICAgICAicm9sZSI6ICJNYWpvciBEb21vIiwKICAgICAgInNvdXJjZSI6IHsKICAgICAgICAiaWQiOiAiaHR0cHM6Ly9zY2hvb2wuZWR1L2lzc3VlcnMvMjAxMjM0IiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJQcm9maWxlIgogICAgICAgIF0sCiAgICAgICAgIm5hbWUiOiAiMUVkVGVjaCBDb2xsZWdlIG9mIEFydHMiCiAgICAgIH0sCiAgICAgICJ0ZXJtIjogIkZhbGwiLAogICAgICAiaWRlbnRpZmllciI6IFsKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6ICJJZGVudGl0eU9iamVjdCIsCiAgICAgICAgICAiaWRlbnRpdHlIYXNoIjogInN0dWRlbnRAMWVkdGVjaC5lZHUiLAogICAgICAgICAgImlkZW50aXR5VHlwZSI6ICJlbWFpbEFkZHJlc3MiLAogICAgICAgICAgImhhc2hlZCI6IGZhbHNlLAogICAgICAgICAgInNhbHQiOiAibm90LXVzZWQiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6ICJJZGVudGl0eU9iamVjdCIsCiAgICAgICAgICAiaWRlbnRpdHlIYXNoIjogInNvbWVib2R5QGdtYWlsLmNvbSIsCiAgICAgICAgICAiaWRlbnRpdHlUeXBlIjogImVtYWlsQWRkcmVzcyIsCiAgICAgICAgICAiaGFzaGVkIjogZmFsc2UsCiAgICAgICAgICAic2FsdCI6ICJub3QtdXNlZCIKICAgICAgICB9CiAgICAgIF0sCiAgICAgICJhY2hpZXZlbWVudCI6IHsKICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9hY2hpZXZlbWVudHMvZGVncmVlIiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJBY2hpZXZlbWVudCIKICAgICAgICBdLAogICAgICAgICJhbGlnbm1lbnQiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJBbGlnbm1lbnQiCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogImRlZ3JlZSIsCiAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgRGVncmVlIHByb2dyYW1zLiIsCiAgICAgICAgICAgICJ0YXJnZXROYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUiLAogICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICJ0YXJnZXRUeXBlIjogIkNGSXRlbSIsCiAgICAgICAgICAgICJ0YXJnZXRVcmwiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9jYXRhbG9nL2RlZ3JlZSIKICAgICAgICAgIH0sCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJBbGlnbm1lbnQiCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogImRlZ3JlZSIsCiAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgRGVncmVlIHByb2dyYW1zLiIsCiAgICAgICAgICAgICJ0YXJnZXROYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUiLAogICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICJ0YXJnZXRUeXBlIjogIkNUREwiLAogICAgICAgICAgICAidGFyZ2V0VXJsIjogImh0dHBzOi8vY3JlZGVudGlhbGVuZ2luZXJlZ2lzdHJ5Lm9yZy9yZXNvdXJjZXMvY2UtOThjYjAyN2ItOTVlZi00NDk0LTkwOGQtNmY3NzkwZWM2YjZiIgogICAgICAgICAgfQogICAgICAgIF0sCiAgICAgICAgImFjaGlldmVtZW50VHlwZSI6ICJEZWdyZWUiLAogICAgICAgICJjcmVhdG9yIjogewogICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgXSwKICAgICAgICAgICJuYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSIsCiAgICAgICAgICAidXJsIjogImh0dHBzOi8vMWVkdGVjaC5lZHUiLAogICAgICAgICAgInBob25lIjogIjEtMjIyLTMzMy00NDQ0IiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgcHJvdmlkZXMgb25saW5lIGRlZ3JlZSBwcm9ncmFtcy4iLAogICAgICAgICAgImVuZG9yc2VtZW50IjogWwogICAgICAgICAgICB7CiAgICAgICAgICAgICAgIkBjb250ZXh0IjogWwogICAgICAgICAgICAgICAgImh0dHBzOi8vd3d3LnczLm9yZy9ucy9jcmVkZW50aWFscy92MiIsCiAgICAgICAgICAgICAgICAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjMuanNvbiIKICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvZW5kb3JzZW1lbnRjcmVkZW50aWFsLzM3MzIiLAogICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgIlZlcmlmaWFibGVDcmVkZW50aWFsIiwKICAgICAgICAgICAgICAgICJFbmRvcnNlbWVudENyZWRlbnRpYWwiCiAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAibmFtZSI6ICJTREUgZW5kb3JzZW1lbnQiLAogICAgICAgICAgICAgICJpc3N1ZXIiOiB7CiAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSIsCiAgICAgICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAgICAgIlByb2ZpbGUiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgIm5hbWUiOiAiRXhhbXBsZSBBY2NyZWRpdGluZyBBZ2VuY3kiCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAidmFsaWRGcm9tIjogIjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwKICAgICAgICAgICAgICAidmFsaWRVbnRpbCI6ICIyMDIwLTAxLTAxVDAwOjAwOjAwWiIsCiAgICAgICAgICAgICAgImNyZWRlbnRpYWxTdWJqZWN0IjogewogICAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgICAgICJFbmRvcnNlbWVudFN1YmplY3QiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgImVuZG9yc2VtZW50Q29tbWVudCI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgaXMgaW4gZ29vZCBzdGFuZGluZyIKICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICJjcmVkZW50aWFsU2NoZW1hIjogWwogICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL3NjaGVtYS9qc29uL29iX3YzcDBfZW5kb3JzZW1lbnRjcmVkZW50aWFsX3NjaGVtYS5qc29uIiwKICAgICAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaEpzb25TY2hlbWFWYWxpZGF0b3IyMDE5IgogICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vYWNjcmVkaXRlci5lZHUvc2NoZW1hL2VuZG9yc2VtZW50Y3JlZGVudGlhbC5qc29uIiwKICAgICAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaEpzb25TY2hlbWFWYWxpZGF0b3IyMDE5IgogICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgImNyZWRlbnRpYWxTdGF0dXMiOiB7CiAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9jcmVkZW50aWFscy8zNzMyL3Jldm9jYXRpb25zIiwKICAgICAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hSZXZvY2F0aW9uTGlzdCIKICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICJyZWZyZXNoU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMiIsCiAgICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoQ3JlZGVudGlhbFJlZnJlc2giCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAicHJvb2YiOiBbCiAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICJ0eXBlIjogIkRhdGFJbnRlZ3JpdHlQcm9vZiIsCiAgICAgICAgICAgICAgICAgICJjcnlwdG9zdWl0ZSI6ICJlZGRzYS1yZGYtMjAyMiIsCiAgICAgICAgICAgICAgICAgICJjcmVhdGVkIjogIjIwMjItMDUtMjZUMTg6MTc6MDhaIiwKICAgICAgICAgICAgICAgICAgInZlcmlmaWNhdGlvbk1ldGhvZCI6ICJodHRwczovL2FjY3JlZGl0ZXIuZWR1L2lzc3VlcnMvNTY1MDQ5I3p2UGtRaVVGZkpyZ25DUmh5UGtUU2tnckdYYm5MUjE1cEhINUhaVllOZE00VENBd1FIcUc3Zk1lTVBMdFlOUm5FZ29WMWFKZFI1RTYxZVd1NXNXUllndEEiLAogICAgICAgICAgICAgICAgICAicHJvb2ZQdXJwb3NlIjogImFzc2VydGlvbk1ldGhvZCIsCiAgICAgICAgICAgICAgICAgICJwcm9vZlZhbHVlIjogInp2UGtRaVVGZkpyZ25DUmh5UGtUU2tnckdYYm5MUjE1cEhINUhaVllOZE00VENBd1FIcUc3Zk1lTVBMdFlOUm5FZ29WMWFKZFI1RTYxZVd1NXNXUllndEEiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgXQogICAgICAgICAgICB9LAogICAgICAgICAgICB7CiAgICAgICAgICAgICAgIkBjb250ZXh0IjogWwogICAgICAgICAgICAgICAgImh0dHBzOi8vd3d3LnczLm9yZy9ucy9jcmVkZW50aWFscy92MiIsCiAgICAgICAgICAgICAgICAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjMuanNvbiIKICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvZW5kb3JzZW1lbnRjcmVkZW50aWFsLzM3MzMiLAogICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgIlZlcmlmaWFibGVDcmVkZW50aWFsIiwKICAgICAgICAgICAgICAgICJFbmRvcnNlbWVudENyZWRlbnRpYWwiCiAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAibmFtZSI6ICJTREUgZW5kb3JzZW1lbnQiLAogICAgICAgICAgICAgICJpc3N1ZXIiOiB7CiAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9zdGF0ZS5nb3YvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICJuYW1lIjogIlN0YXRlIERlcGFydG1lbnQgb2YgRWR1Y2F0aW9uIgogICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgInZhbGlkRnJvbSI6ICIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsCiAgICAgICAgICAgICAgInZhbGlkVW50aWwiOiAiMjAyMC0wMS0wMVQwMDowMDowMFoiLAogICAgICAgICAgICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgICAgICAgICAgICJpZCI6ICJodHRwczovLzFlZHRlY2guZWR1L2lzc3VlcnMvNTY1MDQ5IiwKICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAiRW5kb3JzZW1lbnRTdWJqZWN0IgogICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICJlbmRvcnNlbWVudENvbW1lbnQiOiAiMUVkVGVjaCBVbml2ZXJzaXR5IGlzIGluIGdvb2Qgc3RhbmRpbmciCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAiY3JlZGVudGlhbFNjaGVtYSI6IFsKICAgICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9zY2hlbWEvanNvbi9vYl92M3AwX2VuZG9yc2VtZW50Y3JlZGVudGlhbF9zY2hlbWEuanNvbiIsCiAgICAgICAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hKc29uU2NoZW1hVmFsaWRhdG9yMjAxOSIKICAgICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAgICJpZCI6ICJodHRwczovL3N0YXRlLmdvdi9zY2hlbWEvZW5kb3JzZW1lbnRjcmVkZW50aWFsLmpzb24iLAogICAgICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoSnNvblNjaGVtYVZhbGlkYXRvcjIwMTkiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAiY3JlZGVudGlhbFN0YXR1cyI6IHsKICAgICAgICAgICAgICAgICJpZCI6ICJodHRwczovL3N0YXRlLmdvdi9jcmVkZW50aWFscy8zNzMyL3Jldm9jYXRpb25zIiwKICAgICAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hSZXZvY2F0aW9uTGlzdCIKICAgICAgICAgICAgICB9LAogICAgICAgICAgICAgICJyZWZyZXNoU2VydmljZSI6IHsKICAgICAgICAgICAgICAgICJpZCI6ICJodHRwOi8vc3RhdGUuZ292L2NyZWRlbnRpYWxzLzM3MzIiLAogICAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaENyZWRlbnRpYWxSZWZyZXNoIgogICAgICAgICAgICAgIH0sCiAgICAgICAgICAgICAgInByb29mIjogWwogICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICAgICAgICAgICAiY3J5cHRvc3VpdGUiOiAiZWRkc2EtcmRmLTIwMjIiLAogICAgICAgICAgICAgICAgICAiY3JlYXRlZCI6ICIyMDIyLTA1LTI2VDE4OjI1OjU5WiIsCiAgICAgICAgICAgICAgICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSN6NWJEbm1TZ0Rjelh3Wkd5YTZaanhLYXhrZEt4enNDTWlWU3NnRVZXeG5hV0s3WnFiS256Y0NkN21VS0U5RFFhQUwyUU1YUDVBcXVQZVc2VzJDV3JaN2pOQyIsCiAgICAgICAgICAgICAgICAgICJwcm9vZlB1cnBvc2UiOiAiYXNzZXJ0aW9uTWV0aG9kIiwKICAgICAgICAgICAgICAgICAgInByb29mVmFsdWUiOiAiejViRG5tU2dEY3pYd1pHeWE2Wmp4S2F4a2RLeHpzQ01pVlNzZ0VWV3huYVdLN1pxYktuemNDZDdtVUtFOURRYUFMMlFNWFA1QXF1UGVXNlcyQ1dyWjdqTkMiCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgXQogICAgICAgICAgICB9CiAgICAgICAgICBdLAogICAgICAgICAgImltYWdlIjogewogICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9sb2dvLnBuZyIsCiAgICAgICAgICAgICJ0eXBlIjogIkltYWdlIiwKICAgICAgICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IGxvZ28iCiAgICAgICAgICB9LAogICAgICAgICAgImVtYWlsIjogInJlZ2lzdHJhckAxZWR0ZWNoLmVkdSIsCiAgICAgICAgICAiYWRkcmVzcyI6IHsKICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgIkFkZHJlc3MiCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJhZGRyZXNzQ291bnRyeSI6ICJVU0EiLAogICAgICAgICAgICAiYWRkcmVzc0NvdW50cnlDb2RlIjogIlVTIiwKICAgICAgICAgICAgImFkZHJlc3NSZWdpb24iOiAiVFgiLAogICAgICAgICAgICAiYWRkcmVzc0xvY2FsaXR5IjogIkF1c3RpbiIsCiAgICAgICAgICAgICJzdHJlZXRBZGRyZXNzIjogIjEyMyBGaXJzdCBTdCIsCiAgICAgICAgICAgICJwb3N0T2ZmaWNlQm94TnVtYmVyIjogIjEiLAogICAgICAgICAgICAicG9zdGFsQ29kZSI6ICIxMjM0NSIsCiAgICAgICAgICAgICJnZW8iOiB7CiAgICAgICAgICAgICAgInR5cGUiOiAiR2VvQ29vcmRpbmF0ZXMiLAogICAgICAgICAgICAgICJsYXRpdHVkZSI6IDEsCiAgICAgICAgICAgICAgImxvbmdpdHVkZSI6IDEKICAgICAgICAgICAgfQogICAgICAgICAgfSwKICAgICAgICAgICJvdGhlcklkZW50aWZpZXIiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAidHlwZSI6ICJJZGVudGlmaWVyRW50cnkiLAogICAgICAgICAgICAgICJpZGVudGlmaWVyIjogIjEyMzQ1IiwKICAgICAgICAgICAgICAiaWRlbnRpZmllclR5cGUiOiAic291cmNlZElkIgogICAgICAgICAgICB9LAogICAgICAgICAgICB7CiAgICAgICAgICAgICAgInR5cGUiOiAiSWRlbnRpZmllckVudHJ5IiwKICAgICAgICAgICAgICAiaWRlbnRpZmllciI6ICI2Nzg5MCIsCiAgICAgICAgICAgICAgImlkZW50aWZpZXJUeXBlIjogIm5hdGlvbmFsSWRlbnRpdHlOdW1iZXIiCiAgICAgICAgICAgIH0KICAgICAgICAgIF0sCiAgICAgICAgICAib2ZmaWNpYWwiOiAiSG9yYWNlIE1hbm4iLAogICAgICAgICAgInBhcmVudE9yZyI6IHsKICAgICAgICAgICAgImlkIjogImRpZDpleGFtcGxlOjEyMzQ1Njc4OSIsCiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgICBdLAogICAgICAgICAgICAibmFtZSI6ICJVbml2ZXJzYWwgVW5pdmVyc2l0aWVzIgogICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgImNyZWRpdHNBdmFpbGFibGUiOiAzNiwKICAgICAgICAiY3JpdGVyaWEiOiB7CiAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9hY2hpZXZlbWVudHMvZGVncmVlIiwKICAgICAgICAgICJuYXJyYXRpdmUiOiAiIyBEZWdyZWUgUmVxdWlyZW1lbnRzXG5TdHVkZW50cyBtdXN0IGNvbXBsZXRlLi4uIgogICAgICAgIH0sCiAgICAgICAgImRlc2NyaXB0aW9uIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUgRGVzY3JpcHRpb24iLAogICAgICAgICJlbmRvcnNlbWVudCI6IFsKICAgICAgICAgIHsKICAgICAgICAgICAgIkBjb250ZXh0IjogWwogICAgICAgICAgICAgICJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLAogICAgICAgICAgICAgICJodHRwczovL3B1cmwuaW1zZ2xvYmFsLm9yZy9zcGVjL29iL3YzcDAvY29udGV4dC0zLjAuMy5qc29uIgogICAgICAgICAgICBdLAogICAgICAgICAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2VuZG9yc2VtZW50Y3JlZGVudGlhbC8zNzM0IiwKICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgIlZlcmlmaWFibGVDcmVkZW50aWFsIiwKICAgICAgICAgICAgICAiRW5kb3JzZW1lbnRDcmVkZW50aWFsIgogICAgICAgICAgICBdLAogICAgICAgICAgICAibmFtZSI6ICJFQUEgZW5kb3JzZW1lbnQiLAogICAgICAgICAgICAiaXNzdWVyIjogewogICAgICAgICAgICAgICJpZCI6ICJodHRwczovL2FjY3JlZGl0ZXIuZWR1L2lzc3VlcnMvNTY1MDQ5IiwKICAgICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgIm5hbWUiOiAiRXhhbXBsZSBBY2NyZWRpdGluZyBBZ2VuY3kiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJ2YWxpZEZyb20iOiAiMjAxMC0wMS0wMVQwMDowMDowMFoiLAogICAgICAgICAgICAidmFsaWRVbnRpbCI6ICIyMDIwLTAxLTAxVDAwOjAwOjAwWiIsCiAgICAgICAgICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9pc3N1ZXJzLzU2NTA0OSIsCiAgICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgICAiRW5kb3JzZW1lbnRTdWJqZWN0IgogICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgImVuZG9yc2VtZW50Q29tbWVudCI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgaXMgaW4gZ29vZCBzdGFuZGluZyIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgImNyZWRlbnRpYWxTY2hlbWEiOiBbCiAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9zY2hlbWEvanNvbi9vYl92M3AwX2VuZG9yc2VtZW50Y3JlZGVudGlhbF9zY2hlbWEuanNvbiIsCiAgICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoSnNvblNjaGVtYVZhbGlkYXRvcjIwMTkiCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9zY2hlbWEvZW5kb3JzZW1lbnRjcmVkZW50aWFsLmpzb24iLAogICAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaEpzb25TY2hlbWFWYWxpZGF0b3IyMDE5IgogICAgICAgICAgICAgIH0KICAgICAgICAgICAgXSwKICAgICAgICAgICAgImNyZWRlbnRpYWxTdGF0dXMiOiB7CiAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMi9yZXZvY2F0aW9ucyIsCiAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaFJldm9jYXRpb25MaXN0IgogICAgICAgICAgICB9LAogICAgICAgICAgICAicmVmcmVzaFNlcnZpY2UiOiB7CiAgICAgICAgICAgICAgImlkIjogImh0dHA6Ly8xZWR0ZWNoLmVkdS9jcmVkZW50aWFscy8zNzMyIiwKICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoQ3JlZGVudGlhbFJlZnJlc2giCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJwcm9vZiI6IFsKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICAgICAgICAgImNyeXB0b3N1aXRlIjogImVkZHNhLXJkZi0yMDIyIiwKICAgICAgICAgICAgICAgICJjcmVhdGVkIjogIjIwMjItMDUtMjZUMTg6MTc6MDhaIiwKICAgICAgICAgICAgICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSN6dlBrUWlVRmZKcmduQ1JoeVBrVFNrZ3JHWGJuTFIxNXBISDVIWlZZTmRNNFRDQXdRSHFHN2ZNZU1QTHRZTlJuRWdvVjFhSmRSNUU2MWVXdTVzV1JZZ3RBIiwKICAgICAgICAgICAgICAgICJwcm9vZlB1cnBvc2UiOiAiYXNzZXJ0aW9uTWV0aG9kIiwKICAgICAgICAgICAgICAgICJwcm9vZlZhbHVlIjogInp2UGtRaVVGZkpyZ25DUmh5UGtUU2tnckdYYm5MUjE1cEhINUhaVllOZE00VENBd1FIcUc3Zk1lTVBMdFlOUm5FZ29WMWFKZFI1RTYxZVd1NXNXUllndEEiCiAgICAgICAgICAgICAgfQogICAgICAgICAgICBdCiAgICAgICAgICB9CiAgICAgICAgXSwKICAgICAgICAiZmllbGRPZlN0dWR5IjogIlJlc2VhcmNoIiwKICAgICAgICAiaHVtYW5Db2RlIjogIlIxIiwKICAgICAgICAiaW1hZ2UiOiB7CiAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL2hhbXJ0L2NyZWRlbnRpYWwtY29udmVydGVyL3JlZnMvaGVhZHMvaW1hZ2UvdGVzdC9lZHViYWRnZXNfMTAweDEwMC5wbmciLAogICAgICAgICAgInR5cGUiOiAiSW1hZ2UiLAogICAgICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IERlZ3JlZSIKICAgICAgICB9LAogICAgICAgICJuYW1lIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBEZWdyZWUiLAogICAgICAgICJvdGhlcklkZW50aWZpZXIiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0eXBlIjogIklkZW50aWZpZXJFbnRyeSIsCiAgICAgICAgICAgICJpZGVudGlmaWVyIjogImFiZGUiLAogICAgICAgICAgICAiaWRlbnRpZmllclR5cGUiOiAiaWRlbnRpZmllciIKICAgICAgICAgIH0KICAgICAgICBdLAogICAgICAgICJyZXN1bHREZXNjcmlwdGlvbiI6IFsKICAgICAgICAgIHsKICAgICAgICAgICAgImlkIjogInVybjp1dWlkOmY2YWIyNGNkLTg2ZTgtNGVhZi1iOGM2LWRlZDc0ZThmZDQxYyIsCiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJSZXN1bHREZXNjcmlwdGlvbiIKICAgICAgICAgICAgXSwKICAgICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICAgIF0sCiAgICAgICAgICAgICAgICAidGFyZ2V0Q29kZSI6ICJwcm9qZWN0IiwKICAgICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAgICJ0YXJnZXROYW1lIjogIkZpbmFsIFByb2plY3QiLAogICAgICAgICAgICAgICAgInRhcmdldEZyYW1ld29yayI6ICIxRWRUZWNoIFVuaXZlcnNpdHkgUHJvZ3JhbSBhbmQgQ291cnNlIENhdGFsb2ciLAogICAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZJdGVtIiwKICAgICAgICAgICAgICAgICJ0YXJnZXRVcmwiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9jYXRhbG9nL2RlZ3JlZS9wcm9qZWN0IgogICAgICAgICAgICAgIH0KICAgICAgICAgICAgXSwKICAgICAgICAgICAgImFsbG93ZWRWYWx1ZSI6IFsKICAgICAgICAgICAgICAiRCIsCiAgICAgICAgICAgICAgIkMiLAogICAgICAgICAgICAgICJCIiwKICAgICAgICAgICAgICAiQSIKICAgICAgICAgICAgXSwKICAgICAgICAgICAgIm5hbWUiOiAiRmluYWwgUHJvamVjdCBHcmFkZSIsCiAgICAgICAgICAgICJyZXF1aXJlZFZhbHVlIjogIkMiLAogICAgICAgICAgICAicmVzdWx0VHlwZSI6ICJMZXR0ZXJHcmFkZSIKICAgICAgICAgIH0sCiAgICAgICAgICB7CiAgICAgICAgICAgICJpZCI6ICJ1cm46dXVpZDphNzBkZGM2YS00YzRhLTRiZDgtODI3Ny1jYjk3Yzc5ZjQwYzUiLAogICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAiUmVzdWx0RGVzY3JpcHRpb24iCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJhbGlnbm1lbnQiOiBbCiAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICAgICAgICJBbGlnbm1lbnQiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgInRhcmdldENvZGUiOiAicHJvamVjdCIsCiAgICAgICAgICAgICAgICAidGFyZ2V0RGVzY3JpcHRpb24iOiAiUHJvamVjdCBkZXNjcmlwdGlvbiIsCiAgICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAgICJ0YXJnZXRGcmFtZXdvcmsiOiAiMUVkVGVjaCBVbml2ZXJzaXR5IFByb2dyYW0gYW5kIENvdXJzZSBDYXRhbG9nIiwKICAgICAgICAgICAgICAgICJ0YXJnZXRUeXBlIjogIkNGSXRlbSIsCiAgICAgICAgICAgICAgICAidGFyZ2V0VXJsIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY2F0YWxvZy9kZWdyZWUvcHJvamVjdCIKICAgICAgICAgICAgICB9CiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJhbGxvd2VkVmFsdWUiOiBbCiAgICAgICAgICAgICAgIkQiLAogICAgICAgICAgICAgICJDIiwKICAgICAgICAgICAgICAiQiIsCiAgICAgICAgICAgICAgIkEiCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJuYW1lIjogIkZpbmFsIFByb2plY3QgR3JhZGUiLAogICAgICAgICAgICAicmVxdWlyZWRMZXZlbCI6ICJ1cm46dXVpZDpkMDVhMDg2Ny1kMGFkLTRiMDMtYmRiNS0yOGZiNWQyYWFiN2EiLAogICAgICAgICAgICAicmVzdWx0VHlwZSI6ICJSdWJyaWNDcml0ZXJpb25MZXZlbCIsCiAgICAgICAgICAgICJydWJyaWNDcml0ZXJpb25MZXZlbCI6IFsKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAiaWQiOiAidXJuOnV1aWQ6ZDA1YTA4NjctZDBhZC00YjAzLWJkYjUtMjhmYjVkMmFhYjdhIiwKICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAiUnVicmljQ3JpdGVyaW9uTGV2ZWwiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogInByb2plY3QiLAogICAgICAgICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZSdWJyaWNDcml0ZXJpb25MZXZlbCIsCiAgICAgICAgICAgICAgICAgICAgInRhcmdldFVybCI6ICJodHRwczovLzFlZHRlY2guZWR1L2NhdGFsb2cvZGVncmVlL3Byb2plY3QvcnVicmljL2xldmVscy9tYXN0ZXJlZCIKICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJUaGUgYXV0aG9yIGRlbW9uc3RyYXRlZC4uLiIsCiAgICAgICAgICAgICAgICAibGV2ZWwiOiAiTWFzdGVyZWQiLAogICAgICAgICAgICAgICAgIm5hbWUiOiAiTWFzdGVyeSIsCiAgICAgICAgICAgICAgICAicG9pbnRzIjogIjQiCiAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICB7CiAgICAgICAgICAgICAgICAiaWQiOiAidXJuOnV1aWQ6NmI4NGI0MjktMzFlZS00ZGFjLTlkMjAtZTVjNTU4ODFmODBlIiwKICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAiUnVicmljQ3JpdGVyaW9uTGV2ZWwiCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogInByb2plY3QiLAogICAgICAgICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZSdWJyaWNDcml0ZXJpb25MZXZlbCIsCiAgICAgICAgICAgICAgICAgICAgInRhcmdldFVybCI6ICJodHRwczovLzFlZHRlY2guZWR1L2NhdGFsb2cvZGVncmVlL3Byb2plY3QvcnVicmljL2xldmVscy9iYXNpYyIKICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgXSwKICAgICAgICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJUaGUgYXV0aG9yIGRlbW9uc3RyYXRlZC4uLiIsCiAgICAgICAgICAgICAgICAibGV2ZWwiOiAiQmFzaWMiLAogICAgICAgICAgICAgICAgIm5hbWUiOiAiQmFzaWMiLAogICAgICAgICAgICAgICAgInBvaW50cyI6ICI0IgogICAgICAgICAgICAgIH0KICAgICAgICAgICAgXQogICAgICAgICAgfSwKICAgICAgICAgIHsKICAgICAgICAgICAgImlkIjogInVybjp1dWlkOmIwN2MwMzg3LWYyZDYtNGI2NS1hM2Y0LWY0ZTQzMDJlYThmNyIsCiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJSZXN1bHREZXNjcmlwdGlvbiIKICAgICAgICAgICAgXSwKICAgICAgICAgICAgIm5hbWUiOiAiUHJvamVjdCBTdGF0dXMiLAogICAgICAgICAgICAicmVzdWx0VHlwZSI6ICJTdGF0dXMiCiAgICAgICAgICB9CiAgICAgICAgXSwKICAgICAgICAic3BlY2lhbGl6YXRpb24iOiAiQ29tcHV0ZXIgU2NpZW5jZSBSZXNlYXJjaCIsCiAgICAgICAgInRhZyI6IFsKICAgICAgICAgICJyZXNlYXJjaCIsCiAgICAgICAgICAiY29tcHV0ZXIgc2NpZW5jZSIKICAgICAgICBdCiAgICAgIH0sCiAgICAgICJpbWFnZSI6IHsKICAgICAgICAiaWQiOiAiaHR0cHM6Ly8xZWR0ZWNoLmVkdS9jcmVkZW50aWFscy8zNzMyL2ltYWdlIiwKICAgICAgICAidHlwZSI6ICJJbWFnZSIsCiAgICAgICAgImNhcHRpb24iOiAiMUVkVGVjaCBVbml2ZXJzaXR5IERlZ3JlZSBmb3IgRXhhbXBsZSBTdHVkZW50IgogICAgICB9LAogICAgICAibmFycmF0aXZlIjogIlRoZXJlIGlzIGEgZmluYWwgcHJvamVjdCByZXBvcnQgYW5kIHNvdXJjZSBjb2RlIGV2aWRlbmNlLiIsCiAgICAgICJyZXN1bHQiOiBbCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICJSZXN1bHQiCiAgICAgICAgICBdLAogICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgewogICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogInByb2plY3QiLAogICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZJdGVtIiwKICAgICAgICAgICAgICAidGFyZ2V0VXJsIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY2F0YWxvZy9kZWdyZWUvcHJvamVjdC9yZXN1bHQvMSIKICAgICAgICAgICAgfQogICAgICAgICAgXSwKICAgICAgICAgICJyZXN1bHREZXNjcmlwdGlvbiI6ICJ1cm46dXVpZDpmNmFiMjRjZC04NmU4LTRlYWYtYjhjNi1kZWQ3NGU4ZmQ0MWMiLAogICAgICAgICAgInZhbHVlIjogIkEiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgIlJlc3VsdCIKICAgICAgICAgIF0sCiAgICAgICAgICAiYWNoaWV2ZWRMZXZlbCI6ICJ1cm46dXVpZDpkMDVhMDg2Ny1kMGFkLTRiMDMtYmRiNS0yOGZiNWQyYWFiN2EiLAogICAgICAgICAgImFsaWdubWVudCI6IFsKICAgICAgICAgICAgewogICAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICAgIkFsaWdubWVudCIKICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICJ0YXJnZXRDb2RlIjogInByb2plY3QiLAogICAgICAgICAgICAgICJ0YXJnZXREZXNjcmlwdGlvbiI6ICJQcm9qZWN0IGRlc2NyaXB0aW9uIiwKICAgICAgICAgICAgICAidGFyZ2V0TmFtZSI6ICJGaW5hbCBQcm9qZWN0IiwKICAgICAgICAgICAgICAidGFyZ2V0RnJhbWV3b3JrIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBQcm9ncmFtIGFuZCBDb3Vyc2UgQ2F0YWxvZyIsCiAgICAgICAgICAgICAgInRhcmdldFR5cGUiOiAiQ0ZJdGVtIiwKICAgICAgICAgICAgICAidGFyZ2V0VXJsIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY2F0YWxvZy9kZWdyZWUvcHJvamVjdC9yZXN1bHQvMSIKICAgICAgICAgICAgfQogICAgICAgICAgXSwKICAgICAgICAgICJyZXN1bHREZXNjcmlwdGlvbiI6ICJ1cm46dXVpZDpmNmFiMjRjZC04NmU4LTRlYWYtYjhjNi1kZWQ3NGU4ZmQ0MWMiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgIlJlc3VsdCIKICAgICAgICAgIF0sCiAgICAgICAgICAicmVzdWx0RGVzY3JpcHRpb24iOiAidXJuOnV1aWQ6ZjZhYjI0Y2QtODZlOC00ZWFmLWI4YzYtZGVkNzRlOGZkNDFjIiwKICAgICAgICAgICJzdGF0dXMiOiAiQ29tcGxldGVkIgogICAgICAgIH0KICAgICAgXQogICAgfSwKICAgICJlbmRvcnNlbWVudCI6IFsKICAgICAgewogICAgICAgICJAY29udGV4dCI6IFsKICAgICAgICAgICJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLAogICAgICAgICAgImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9jb250ZXh0LTMuMC4zLmpzb24iCiAgICAgICAgXSwKICAgICAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2VuZG9yc2VtZW50Y3JlZGVudGlhbC8zNzM1IiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsCiAgICAgICAgICAiRW5kb3JzZW1lbnRDcmVkZW50aWFsIgogICAgICAgIF0sCiAgICAgICAgIm5hbWUiOiAiRUFBIGVuZG9yc2VtZW50IiwKICAgICAgICAiaXNzdWVyIjogewogICAgICAgICAgImlkIjogImh0dHBzOi8vYWNjcmVkaXRlci5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgXSwKICAgICAgICAgICJuYW1lIjogIkV4YW1wbGUgQWNjcmVkaXRpbmcgQWdlbmN5IgogICAgICAgIH0sCiAgICAgICAgInZhbGlkRnJvbSI6ICIyMDEwLTAxLTAxVDAwOjAwOjAwWiIsCiAgICAgICAgInZhbGlkVW50aWwiOiAiMjAyMC0wMS0wMVQwMDowMDowMFoiLAogICAgICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgICAgICJpZCI6ICJodHRwczovLzFlZHRlY2guZWR1L2lzc3VlcnMvNTY1MDQ5IiwKICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAiRW5kb3JzZW1lbnRTdWJqZWN0IgogICAgICAgICAgXSwKICAgICAgICAgICJlbmRvcnNlbWVudENvbW1lbnQiOiAiMUVkVGVjaCBVbml2ZXJzaXR5IGlzIGluIGdvb2Qgc3RhbmRpbmciCiAgICAgICAgfSwKICAgICAgICAiY3JlZGVudGlhbFNjaGVtYSI6IFsKICAgICAgICAgIHsKICAgICAgICAgICAgImlkIjogImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9zY2hlbWEvanNvbi9vYl92M3AwX2VuZG9yc2VtZW50Y3JlZGVudGlhbF9zY2hlbWEuanNvbiIsCiAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hKc29uU2NoZW1hVmFsaWRhdG9yMjAxOSIKICAgICAgICAgIH0sCiAgICAgICAgICB7CiAgICAgICAgICAgICJpZCI6ICJodHRwczovL2FjY3JlZGl0ZXIuZWR1L3NjaGVtYS9lbmRvcnNlbWVudGNyZWRlbnRpYWwuanNvbiIsCiAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hKc29uU2NoZW1hVmFsaWRhdG9yMjAxOSIKICAgICAgICAgIH0KICAgICAgICBdLAogICAgICAgICJjcmVkZW50aWFsU3RhdHVzIjogewogICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMi9yZXZvY2F0aW9ucyIsCiAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoUmV2b2NhdGlvbkxpc3QiCiAgICAgICAgfSwKICAgICAgICAicmVmcmVzaFNlcnZpY2UiOiB7CiAgICAgICAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2NyZWRlbnRpYWxzLzM3MzIiLAogICAgICAgICAgInR5cGUiOiAiMUVkVGVjaENyZWRlbnRpYWxSZWZyZXNoIgogICAgICAgIH0sCiAgICAgICAgInByb29mIjogWwogICAgICAgICAgewogICAgICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICAgICAiY3J5cHRvc3VpdGUiOiAiZWRkc2EtcmRmLTIwMjIiLAogICAgICAgICAgICAiY3JlYXRlZCI6ICIyMDIyLTA1LTI2VDE4OjE3OjA4WiIsCiAgICAgICAgICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSN6dlBrUWlVRmZKcmduQ1JoeVBrVFNrZ3JHWGJuTFIxNXBISDVIWlZZTmRNNFRDQXdRSHFHN2ZNZU1QTHRZTlJuRWdvVjFhSmRSNUU2MWVXdTVzV1JZZ3RBIiwKICAgICAgICAgICAgInByb29mUHVycG9zZSI6ICJhc3NlcnRpb25NZXRob2QiLAogICAgICAgICAgICAicHJvb2ZWYWx1ZSI6ICJ6dlBrUWlVRmZKcmduQ1JoeVBrVFNrZ3JHWGJuTFIxNXBISDVIWlZZTmRNNFRDQXdRSHFHN2ZNZU1QTHRZTlJuRWdvVjFhSmRSNUU2MWVXdTVzV1JZZ3RBIgogICAgICAgICAgfQogICAgICAgIF0KICAgICAgfQogICAgXSwKICAgICJldmlkZW5jZSI6IFsKICAgICAgewogICAgICAgICJpZCI6ICJodHRwczovLzFlZHRlY2guZWR1L2NyZWRlbnRpYWxzLzM3MzIvZXZpZGVuY2UvMSIsCiAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAiRXZpZGVuY2UiCiAgICAgICAgXSwKICAgICAgICAibmFycmF0aXZlIjogIiMgRmluYWwgUHJvamVjdCBSZXBvcnQgXG4gVGhpcyBwcm9qZWN0IHdhcyAuLi4iLAogICAgICAgICJuYW1lIjogIkZpbmFsIFByb2plY3QgUmVwb3J0IiwKICAgICAgICAiZGVzY3JpcHRpb24iOiAiVGhpcyBpcyB0aGUgZmluYWwgcHJvamVjdCByZXBvcnQuIiwKICAgICAgICAiZ2VucmUiOiAiUmVzZWFyY2giLAogICAgICAgICJhdWRpZW5jZSI6ICJEZXBhcnRtZW50IgogICAgICB9LAogICAgICB7CiAgICAgICAgImlkIjogImh0dHBzOi8vZ2l0aHViLmNvbS9zb21lYm9keS9wcm9qZWN0IiwKICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICJFdmlkZW5jZSIKICAgICAgICBdLAogICAgICAgICJuYW1lIjogIkZpbmFsIFByb2plY3QgQ29kZSIsCiAgICAgICAgImRlc2NyaXB0aW9uIjogIlRoaXMgaXMgdGhlIHNvdXJjZSBjb2RlIGZvciB0aGUgZmluYWwgcHJvamVjdCBhcHAuIiwKICAgICAgICAiZ2VucmUiOiAiUmVzZWFyY2giLAogICAgICAgICJhdWRpZW5jZSI6ICJEZXBhcnRtZW50IgogICAgICB9CiAgICBdLAogICAgImlzc3VlciI6IHsKICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAidHlwZSI6IFsKICAgICAgICAiUHJvZmlsZSIKICAgICAgXSwKICAgICAgIm5hbWUiOiAiMUVkVGVjaCBVbml2ZXJzaXR5IiwKICAgICAgInVybCI6ICJodHRwczovLzFlZHRlY2guZWR1IiwKICAgICAgInBob25lIjogIjEtMjIyLTMzMy00NDQ0IiwKICAgICAgImRlc2NyaXB0aW9uIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBwcm92aWRlcyBvbmxpbmUgZGVncmVlIHByb2dyYW1zLiIsCiAgICAgICJlbmRvcnNlbWVudCI6IFsKICAgICAgICB7CiAgICAgICAgICAiQGNvbnRleHQiOiBbCiAgICAgICAgICAgICJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLAogICAgICAgICAgICAiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjMuanNvbiIKICAgICAgICAgIF0sCiAgICAgICAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2VuZG9yc2VtZW50Y3JlZGVudGlhbC8zNzM2IiwKICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLAogICAgICAgICAgICAiRW5kb3JzZW1lbnRDcmVkZW50aWFsIgogICAgICAgICAgXSwKICAgICAgICAgICJuYW1lIjogIkVBQSBlbmRvcnNlbWVudCIsCiAgICAgICAgICAiaXNzdWVyIjogewogICAgICAgICAgICAiaWQiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSIsCiAgICAgICAgICAgICJ0eXBlIjogWwogICAgICAgICAgICAgICJQcm9maWxlIgogICAgICAgICAgICBdLAogICAgICAgICAgICAibmFtZSI6ICJFeGFtcGxlIEFjY3JlZGl0aW5nIEFnZW5jeSIKICAgICAgICAgIH0sCiAgICAgICAgICAidmFsaWRGcm9tIjogIjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwKICAgICAgICAgICJ2YWxpZFVudGlsIjogIjIwMjAtMDEtMDFUMDA6MDA6MDBaIiwKICAgICAgICAgICJjcmVkZW50aWFsU3ViamVjdCI6IHsKICAgICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvaXNzdWVycy81NjUwNDkiLAogICAgICAgICAgICAidHlwZSI6IFsKICAgICAgICAgICAgICAiRW5kb3JzZW1lbnRTdWJqZWN0IgogICAgICAgICAgICBdLAogICAgICAgICAgICAiZW5kb3JzZW1lbnRDb21tZW50IjogIjFFZFRlY2ggVW5pdmVyc2l0eSBpcyBpbiBnb29kIHN0YW5kaW5nIgogICAgICAgICAgfSwKICAgICAgICAgICJjcmVkZW50aWFsU2NoZW1hIjogWwogICAgICAgICAgICB7CiAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9zY2hlbWEvanNvbi9vYl92M3AwX2VuZG9yc2VtZW50Y3JlZGVudGlhbF9zY2hlbWEuanNvbiIsCiAgICAgICAgICAgICAgInR5cGUiOiAiMUVkVGVjaEpzb25TY2hlbWFWYWxpZGF0b3IyMDE5IgogICAgICAgICAgICB9LAogICAgICAgICAgICB7CiAgICAgICAgICAgICAgImlkIjogImh0dHBzOi8vYWNjcmVkaXRlci5lZHUvc2NoZW1hL2VuZG9yc2VtZW50Y3JlZGVudGlhbC5qc29uIiwKICAgICAgICAgICAgICAidHlwZSI6ICIxRWRUZWNoSnNvblNjaGVtYVZhbGlkYXRvcjIwMTkiCiAgICAgICAgICAgIH0KICAgICAgICAgIF0sCiAgICAgICAgICAiY3JlZGVudGlhbFN0YXR1cyI6IHsKICAgICAgICAgICAgImlkIjogImh0dHBzOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMi9yZXZvY2F0aW9ucyIsCiAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hSZXZvY2F0aW9uTGlzdCIKICAgICAgICAgIH0sCiAgICAgICAgICAicmVmcmVzaFNlcnZpY2UiOiB7CiAgICAgICAgICAgICJpZCI6ICJodHRwOi8vMWVkdGVjaC5lZHUvY3JlZGVudGlhbHMvMzczMiIsCiAgICAgICAgICAgICJ0eXBlIjogIjFFZFRlY2hDcmVkZW50aWFsUmVmcmVzaCIKICAgICAgICAgIH0sCiAgICAgICAgICAicHJvb2YiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAidHlwZSI6ICJEYXRhSW50ZWdyaXR5UHJvb2YiLAogICAgICAgICAgICAgICJjcnlwdG9zdWl0ZSI6ICJlZGRzYS1yZGYtMjAyMiIsCiAgICAgICAgICAgICAgImNyZWF0ZWQiOiAiMjAyMi0wNS0yNlQxODoxNzowOFoiLAogICAgICAgICAgICAgICJ2ZXJpZmljYXRpb25NZXRob2QiOiAiaHR0cHM6Ly9hY2NyZWRpdGVyLmVkdS9pc3N1ZXJzLzU2NTA0OSN6dlBrUWlVRmZKcmduQ1JoeVBrVFNrZ3JHWGJuTFIxNXBISDVIWlZZTmRNNFRDQXdRSHFHN2ZNZU1QTHRZTlJuRWdvVjFhSmRSNUU2MWVXdTVzV1JZZ3RBIiwKICAgICAgICAgICAgICAicHJvb2ZQdXJwb3NlIjogImFzc2VydGlvbk1ldGhvZCIsCiAgICAgICAgICAgICAgInByb29mVmFsdWUiOiAienZQa1FpVUZmSnJnbkNSaHlQa1RTa2dyR1hibkxSMTVwSEg1SFpWWU5kTTRUQ0F3UUhxRzdmTWVNUEx0WU5SbkVnb1YxYUpkUjVFNjFlV3U1c1dSWWd0QSIKICAgICAgICAgICAgfQogICAgICAgICAgXQogICAgICAgIH0KICAgICAgXSwKICAgICAgImltYWdlIjogewogICAgICAgICJpZCI6ICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vaGFtcnQvY3JlZGVudGlhbC1jb252ZXJ0ZXIvcmVmcy9oZWFkcy9pbWFnZS90ZXN0L2VkdWJhZGdlc18xMDB4MTAwLnBuZyIsCiAgICAgICAgInR5cGUiOiAiSW1hZ2UiLAogICAgICAgICJjYXB0aW9uIjogIjFFZFRlY2ggVW5pdmVyc2l0eSBsb2dvIgogICAgICB9LAogICAgICAiZW1haWwiOiAicmVnaXN0cmFyQDFlZHRlY2guZWR1IiwKICAgICAgImFkZHJlc3MiOiB7CiAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAiQWRkcmVzcyIKICAgICAgICBdLAogICAgICAgICJhZGRyZXNzQ291bnRyeSI6ICJVU0EiLAogICAgICAgICJhZGRyZXNzQ291bnRyeUNvZGUiOiAiVVMiLAogICAgICAgICJhZGRyZXNzUmVnaW9uIjogIlRYIiwKICAgICAgICAiYWRkcmVzc0xvY2FsaXR5IjogIkF1c3RpbiIsCiAgICAgICAgInN0cmVldEFkZHJlc3MiOiAiMTIzIEZpcnN0IFN0IiwKICAgICAgICAicG9zdE9mZmljZUJveE51bWJlciI6ICIxIiwKICAgICAgICAicG9zdGFsQ29kZSI6ICIxMjM0NSIsCiAgICAgICAgImdlbyI6IHsKICAgICAgICAgICJ0eXBlIjogIkdlb0Nvb3JkaW5hdGVzIiwKICAgICAgICAgICJsYXRpdHVkZSI6IDEsCiAgICAgICAgICAibG9uZ2l0dWRlIjogMQogICAgICAgIH0KICAgICAgfSwKICAgICAgIm90aGVySWRlbnRpZmllciI6IFsKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6ICJJZGVudGlmaWVyRW50cnkiLAogICAgICAgICAgImlkZW50aWZpZXIiOiAiMTIzNDUiLAogICAgICAgICAgImlkZW50aWZpZXJUeXBlIjogInNvdXJjZWRJZCIKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICJ0eXBlIjogIklkZW50aWZpZXJFbnRyeSIsCiAgICAgICAgICAiaWRlbnRpZmllciI6ICI2Nzg5MCIsCiAgICAgICAgICAiaWRlbnRpZmllclR5cGUiOiAibmF0aW9uYWxJZGVudGl0eU51bWJlciIKICAgICAgICB9CiAgICAgIF0sCiAgICAgICJvZmZpY2lhbCI6ICJIb3JhY2UgTWFubiIsCiAgICAgICJwYXJlbnRPcmciOiB7CiAgICAgICAgImlkIjogImRpZDpleGFtcGxlOjEyMzQ1Njc4OSIsCiAgICAgICAgInR5cGUiOiBbCiAgICAgICAgICAiUHJvZmlsZSIKICAgICAgICBdLAogICAgICAgICJuYW1lIjogIlVuaXZlcnNhbCBVbml2ZXJzaXRpZXMiCiAgICAgIH0KICAgIH0sCiAgICAidmFsaWRGcm9tIjogIjIwMTAtMDEtMDFUMDA6MDA6MDBaIiwKICAgICJ2YWxpZFVudGlsIjogIjIwMzAtMDEtMDFUMDA6MDA6MDBaIiwKICAgICJjcmVkZW50aWFsU2NoZW1hIjogWwogICAgICB7CiAgICAgICAgImlkIjogImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9zY2hlbWEvanNvbi9vYl92M3AwX2FjaGlldmVtZW50Y3JlZGVudGlhbF9zY2hlbWEuanNvbiIsCiAgICAgICAgInR5cGUiOiAiMUVkVGVjaEpzb25TY2hlbWFWYWxpZGF0b3IyMDE5IgogICAgICB9CiAgICBdLAogICAgImNyZWRlbnRpYWxTdGF0dXMiOiB7CiAgICAgICJpZCI6ICJodHRwczovLzFlZHRlY2guZWR1L2NyZWRlbnRpYWxzLzM3MzIvcmV2b2NhdGlvbnMiLAogICAgICAidHlwZSI6ICIxRWRUZWNoUmV2b2NhdGlvbkxpc3QiCiAgICB9LAogICAgInJlZnJlc2hTZXJ2aWNlIjogewogICAgICAiaWQiOiAiaHR0cDovLzFlZHRlY2guZWR1L2NyZWRlbnRpYWxzLzM3MzIiLAogICAgICAidHlwZSI6ICIxRWRUZWNoQ3JlZGVudGlhbFJlZnJlc2giCiAgICB9LAogICAgInByb29mIjogWwogICAgICB7CiAgICAgICAgInR5cGUiOiAiRGF0YUludGVncml0eVByb29mIiwKICAgICAgICAiY3JlYXRlZCI6ICIyMDI0LTA1LTMxVDE0OjA1OjI1WiIsCiAgICAgICAgInZlcmlmaWNhdGlvbk1ldGhvZCI6ICJodHRwczovLzFlZHRlY2guZWR1L2lzc3VlcnMvNTY1MDQ5I3o2TWtwaFU2UW1vakM2R2RVQk5ZeXBnbkdhaUwyVExpc0xNeHBFMW9aY21LZzdBZCIsCiAgICAgICAgImNyeXB0b3N1aXRlIjogImVkZHNhLXJkZmMtMjAyMiIsCiAgICAgICAgInByb29mUHVycG9zZSI6ICJhc3NlcnRpb25NZXRob2QiLAogICAgICAgICJwcm9vZlZhbHVlIjogIno1QTRaWExKYTRkVUFyVG1wZFA5dm5yWWlqTUxDVDF0UjlLV2FGbUxUMlBlUXAzZ1NuR0E5d3JSSnFySjVaOFlucFZEeFpRV1JHampXTmJqMlBLREplN2R0IgogICAgICB9CiAgICBdCn0K" } \ No newline at end of file From d9acfdb863f77b997503c3ba17beb6575f31130f Mon Sep 17 00:00:00 2001 From: hamrt Date: Tue, 4 Feb 2025 11:52:12 +0100 Subject: [PATCH 36/45] update_test_image --- .../Complete_OpenBadgeCredential_image.json | 6 +++--- test/1EDTECH_logo_100x100.png | Bin 0 -> 11787 bytes 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 test/1EDTECH_logo_100x100.png diff --git a/json/obv3/examples/Complete_OpenBadgeCredential_image.json b/json/obv3/examples/Complete_OpenBadgeCredential_image.json index 053e83e..c85fa18 100644 --- a/json/obv3/examples/Complete_OpenBadgeCredential_image.json +++ b/json/obv3/examples/Complete_OpenBadgeCredential_image.json @@ -12,7 +12,7 @@ "name": "1EdTech University Degree for Example Student", "description": "1EdTech University Degree Description", "image": { - "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/edubadges_100x100.png", + "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/1EDTECH_logo_100x100.png", "type": "Image", "caption": "1EdTech University Degree for Example Student" }, @@ -314,7 +314,7 @@ "fieldOfStudy": "Research", "humanCode": "R1", "image": { - "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/edubadges_100x100.png", + "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/.png", "type": "Image", "caption": "1EdTech University Degree" }, @@ -651,7 +651,7 @@ } ], "image": { - "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/edubadges_100x100.png", + "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/1EDTECH_logo_100x100.png", "type": "Image", "caption": "1EdTech University logo" }, diff --git a/test/1EDTECH_logo_100x100.png b/test/1EDTECH_logo_100x100.png new file mode 100644 index 0000000000000000000000000000000000000000..4535e662ae29147d27a7e09c482f938598b4b256 GIT binary patch literal 11787 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 Date: Tue, 4 Feb 2025 13:20:17 +0100 Subject: [PATCH 37/45] add university of harderwijk test --- .../Complete_OpenBadgeCredential_image.json | 2 +- test/university_of_harderwijk_75x100.jpg | Bin 0 -> 7238 bytes test/uvh_at_microcredential_full_1.json | 125 ++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 test/university_of_harderwijk_75x100.jpg create mode 100644 test/uvh_at_microcredential_full_1.json diff --git a/json/obv3/examples/Complete_OpenBadgeCredential_image.json b/json/obv3/examples/Complete_OpenBadgeCredential_image.json index c85fa18..d77238d 100644 --- a/json/obv3/examples/Complete_OpenBadgeCredential_image.json +++ b/json/obv3/examples/Complete_OpenBadgeCredential_image.json @@ -314,7 +314,7 @@ "fieldOfStudy": "Research", "humanCode": "R1", "image": { - "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/.png", + "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/1EDTECH_logo_100x100.png", "type": "Image", "caption": "1EdTech University Degree" }, diff --git a/test/university_of_harderwijk_75x100.jpg b/test/university_of_harderwijk_75x100.jpg new file mode 100644 index 0000000000000000000000000000000000000000..826e9988e9e84a8259fb0c88028e9e705543eac3 GIT binary patch literal 7238 zcmbVQcRZWj+mBUBTeVk971gS}Yo)4IW3<$!iW;Hhp+Qve(5h8b6)iv1sw$yo%%~B2 zZ?T$KHG&us70H|D_x#@HegAwv@Atm1bFR<%T-Q0*_l*18=TK*;Siq(}4D0~_n3@8l z0000BfSHaHz(CXJXb*r+5Ww_r9RP5p6Z%K@rn~tc8+w|JB7l|xxM>nV&-FjPF<=1W ze|+E2^1on26I!i*=Zf+Q${Gst8uCil{!&yl6z>24%T1e_8>3X81>cR(S3|HnhtA26Ybb8bJ4N(9_YXq^GB6U}T_yk%{py zFr8!iH_rVB%>M?{e_mF+Tso{oWmo{@ox7B9^*?5}QNKx+{g9TP@$pk;enDZ;m*TG_ z->Rx>YU_~o4UO#`o#?Lap5DH(@rlW)>6uv!7Pq{zy0-pnW0SCdaCk&KCY}7Ijnm(B zXzAaK{udt(8XtN_Mg~UazxdG62h)UsgON#8;T)&#BW4$WuIq}CEZp}}@+#ZTiz!*` z@wf(zvhs>6Vru{iUC|;prZ{K0|x*IpipjUSWFKC`A|1y zQ;WPc!sBj#+t?DAkQJSO!e97aed%eU{arCThGD6^{p%+vq5?utHefZ5D>J@HRm+A@ zU@~4?0vE&bXFQ&i=>etI7uDuz8c3#C&3s=hM009>b_X+hwUw^$ky;3$xYgDKvdrH8 z?_aQLlQGP)*?6Jtp^q`dH3e5+ncX+P&?YX{3sD=GA

}-!1oFv1cX@?&6ug!5Jos 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 Date: Tue, 4 Feb 2025 23:44:15 +0100 Subject: [PATCH 38/45] add eqf --- .../custom_mapping_OBv3_ELM_latest.json | 33 ++++-------- src/backend/base64_encode.rs | 2 +- src/backend/elm_mapping_helper.rs | 50 ++++++++++++++++++- src/backend/repository.rs | 47 ++++++++++++++++- src/backend/transformations.rs | 19 +++++++ test/uvh_at_microcredential_full_1.json | 27 ++++++++++ 6 files changed, 152 insertions(+), 26 deletions(-) diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index 0cb4e25..3db1b21 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -228,7 +228,7 @@ "type_": "copy", "source": { "format": "OBv3", - "path": "$.name" + "path": "$.credentialSubject.achievement.name" }, "destination": { "format": "ELM", @@ -261,7 +261,7 @@ "type_": "titleToSpecifiedByObject", "source": { "format": "OBv3", - "path": "$.name" + "path": "$.credentialSubject.achievement.name" }, "destination": { "format": "ELM", @@ -280,39 +280,26 @@ } }, { - "type_": "identifierToObject", + "type_": "eqfToSpecifiedByQualification", "source": { "format": "OBv3", - "datatype": "Student ID", - "path": "$.credentialSubject.identifier[0]" + "path": "$.credentialSubject.achievement.alignment" }, "destination": { "format": "ELM", - "path": "$.credentialSubject.identifier" + "path": "$.credentialSubject.hasClaim[0].specifiedBy.eqfLevel" } }, { "type_": "identifierToObject", "source": { "format": "OBv3", - "datatype": "ext:givenName", - "path": "$.credentialSubject.identifier[1]" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSubject.fullName.en[0]" - } - }, - { - "type_": "identifierToObject", - "source": { - "format": "OBv3", - "datatype": "ext:familyName", - "path": "$.credentialSubject.identifier[2]" + "datatype": "Student ID", + "path": "$.credentialSubject.identifier[0]" }, "destination": { "format": "ELM", - "path": "$.credentialSubject.fullName" + "path": "$.credentialSubject.identifier" } }, { @@ -320,11 +307,11 @@ "source": { "format": "OBv3", "datatype": "ext:fullName", - "path": "$.credentialSubject.identifier[3]" + "path": "$.credentialSubject.identifier[0]" }, "destination": { "format": "ELM", - "path": "$.credentialSubject.fullName" + "path": "$.credentialSubject.fullName.en[0]" } } ] \ No newline at end of file diff --git a/src/backend/base64_encode.rs b/src/backend/base64_encode.rs index 788be0b..e1541d6 100644 --- a/src/backend/base64_encode.rs +++ b/src/backend/base64_encode.rs @@ -279,7 +279,7 @@ pub fn image_to_elm_media_object(image_value: Value) -> Value { } } Err(_e) => { -// eprintln!("Error: {}", _e); + eprintln!("Error: {}", _e); return Value::Null; } }; diff --git a/src/backend/elm_mapping_helper.rs b/src/backend/elm_mapping_helper.rs index 724e375..500a214 100644 --- a/src/backend/elm_mapping_helper.rs +++ b/src/backend/elm_mapping_helper.rs @@ -81,7 +81,7 @@ pub fn title_to_specifiedby(title: Value) -> Value { let json_data = r#" { "id": "urn:epass:learningAchievementSpec:1", - "type": "LearningAchievementSpecification", + "type": "Qualification", "title": { "en": ["Data and soferetware business"] } @@ -158,3 +158,51 @@ pub fn credentialpoint_values_to_object(credits: Value) -> Value { +/// Creates specifiedBy based on input type in string found title +/// +/// # Arguments +/// - `alignment`: an array that could be found in OBv3 but needs to be translated to fit the new structure of ELM. +/// +/// # Returns +/// - Value: The content value Object in ELM format if successful. +pub fn eqf_to_specifiedby_qualification(alignment: Value) -> Value { + //inspect the title object and re write it so it can be reused in ELM for building a creditpoint that cn be used in Specification + //we need to achieve the following structure for a creditpoint: + let json_data = r#" + { + "id": "http://data.europa.eu/snb/eqf/5", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/eqf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["Level 5"] + } + } + "#; + + let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); + println!("{:#?}", alignment); + // Extract the array from the Value + if let Some(array) = alignment.as_array() { + // Find the targetCode where targetType == "ext:EQF" + if let Some(target_code) = array.iter() + .find(|item| item.get("targetType").and_then(|v| v.as_str()) == Some("ext:EQF")) + .and_then(|item| item.get("targetCode").and_then(|v| v.as_str())) + { + parsed_json["id"] = Value::String(format!("http://publications.europa.eu/resource/authority/language/{}", target_code)); + parsed_json["prefLabel"]["en"] = Value::String(format!("Level {}", target_code)); + } else { + println!("targetCode not found."); + return Value::Null; + } + } else { + println!("Error: Data is not an array."); + return Value::Null; + } + + + //println!("{:#?}", parsed_json); + parsed_json +} \ No newline at end of file diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 2d6c640..5220ed6 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -1,7 +1,7 @@ use crate::{ backend::{ base64_encode::{create_display_parameter,image_to_elm_media_object}, - elm_mapping_helper::{address_to_location, title_to_specifiedby, credentialpoint_values_to_object}, + elm_mapping_helper::{address_to_location, credentialpoint_values_to_object, eqf_to_specifiedby_qualification, title_to_specifiedby}, jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, transformations::{DataLocation, DataTypeLocation, StringArrayValue, StringValue, Transformation}, @@ -574,6 +574,51 @@ impl Repository { Some((destination_path, source_path)) } + Transformation::EqfToSpecifiedByQualification { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + // run the source value through a speficfiedby converter to fit the nested objects into a markdown string + let cred_specifiedby_source = json!(eqf_to_specifiedby_qualification(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(cred_specifiedby_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } Transformation::AddressToLocation { type_: transformation, diff --git a/src/backend/transformations.rs b/src/backend/transformations.rs index 8837828..b43cd9b 100644 --- a/src/backend/transformations.rs +++ b/src/backend/transformations.rs @@ -209,6 +209,20 @@ impl CreditToSpecifiedByObject { } +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum EqfToSpecifiedByQualification { + eqfToSpecifiedByQualification, +} + +impl EqfToSpecifiedByQualification { + pub fn apply(&self, value: Value) -> Value { + match self { + EqfToSpecifiedByQualification::eqfToSpecifiedByQualification => value, + } + } +} + #[allow(non_camel_case_types)] #[derive(Serialize, Deserialize, Debug, Clone)] @@ -306,6 +320,11 @@ pub enum Transformation { source: DataLocation, destination: DataLocation, }, + EqfToSpecifiedByQualification{ + type_: EqfToSpecifiedByQualification, + source: DataLocation, + destination: DataLocation, + }, CreditToSpecifiedByObject{ type_: CreditToSpecifiedByObject, source: DataLocation, diff --git a/test/uvh_at_microcredential_full_1.json b/test/uvh_at_microcredential_full_1.json index 24fc11a..3629f4e 100644 --- a/test/uvh_at_microcredential_full_1.json +++ b/test/uvh_at_microcredential_full_1.json @@ -31,6 +31,23 @@ "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/university_of_harderwijk_75x100.jpg", "type": "Image", "caption": "University of Harderwijk logo" + }, + "address": { + "type": [ + "Address" + ], + "addressCountry": "NDL", + "addressCountryCode": "NL", + "addressRegion": "UT", + "addressLocality": "Harderwijk", + "streetAddress": "123 First St", + "postOfficeBoxNumber": "1", + "postalCode": "12345", + "geo": { + "type": "GeoCoordinates", + "latitude": 1, + "longitude": 1 + } } }, "validFrom": "2024-08-30T00:00:00Z", @@ -40,6 +57,15 @@ "type": [ "AchievementSubject" ], + "identifier": [ + { + "type": "IdentityObject", + "identityHash": "student@1edtech.edu", + "identityType": "emailAddress", + "hashed": false, + "salt": "not-used" + } + ], "achievement": { "id": "https://example.com/achievements/ach-876543", "type": [ @@ -58,6 +84,7 @@ "inLanguage": "nl-NL", "educationProgramIdentifier": 20121350, "ECTS": 3.0, + "creditsAvailable": 3, "alignment": [ { From 2aba5e906df85a16727381c75ffdcf413e6bd2ac Mon Sep 17 00:00:00 2001 From: hamrt Date: Wed, 5 Feb 2025 23:59:22 +0100 Subject: [PATCH 39/45] add mapping and error handling --- src/backend/base64_encode.rs | 72 +- src/backend/candidate_value.rs | 2 +- src/backend/desm_mapping.rs | 19 +- src/backend/elm_mapping_helper.rs | 125 ++- src/backend/init_conversion.rs | 11 +- src/backend/repository copy.rs | 1085 ++++++++++++++++++++++++++ src/backend/repository.rs | 135 ++-- src/backend/routes/api.rs | 73 +- src/backend/routes/translate_file.rs | 8 + src/backend/transformations.rs | 8 +- test/encoded_test copy.json | 6 + 11 files changed, 1339 insertions(+), 205 deletions(-) create mode 100644 src/backend/repository copy.rs create mode 100644 test/encoded_test copy.json diff --git a/src/backend/base64_encode.rs b/src/backend/base64_encode.rs index e1541d6..e470402 100644 --- a/src/backend/base64_encode.rs +++ b/src/backend/base64_encode.rs @@ -21,7 +21,6 @@ pub fn decode_json(json: &str) -> Result, Box> { Ok(conv_byte) } - /// Encode json input from Base64, and returns the byte array. /// /// # Arguments @@ -37,7 +36,6 @@ pub fn encode_json_file(json_file: Vec) -> Result> { Ok(base64_string) } - /// Fetches an image from the given URL, encodes it in Base64, and returns the encoded string. /// /// # Arguments @@ -52,10 +50,10 @@ fn encode_image_from_url(url: &str) -> Result> { Ok(resp) => { assert!(resp.has("Content-Length")); let len: usize = resp.header("Content-Length").unwrap().parse()?; - + let mut bytes: Vec = Vec::with_capacity(len); resp.into_reader().take(10_000_000).read_to_end(&mut bytes)?; - + // Encode the image bytes as a Base64 string base64_string = Base64Engine.encode(&bytes); return Ok(base64_string); @@ -63,7 +61,6 @@ fn encode_image_from_url(url: &str) -> Result> { Err(e) => { return Err(Box::new(e)); } - } } @@ -210,7 +207,7 @@ fn set_language(language: &str) -> Result> { Ok(parsed_language_json) } -pub fn image_to_elm_media_object(image_value: Value) -> Value { +pub fn image_to_elm_media_object(image_value: Value) -> Result { //inspect the image object and re write it so it can be reused in ELM //we need to achieve the following structure into the indivudualDisplay array: @@ -279,8 +276,7 @@ pub fn image_to_elm_media_object(image_value: Value) -> Value { } } Err(_e) => { - eprintln!("Error: {}", _e); - return Value::Null; + return Err("encoding of url failed"); } }; // } else if Base64Engine.decode(ob3_image_id).is_ok() { @@ -296,25 +292,26 @@ pub fn image_to_elm_media_object(image_value: Value) -> Value { encoded_string = content_part.to_string(); } else { // println!("Invalid data URI format."); - return Value::Null; + return Err("Invalid data URI format."); } } else { // println!("The `id` is neither a URL nor a Base64-encoded string."); - return Value::Null; + return Err("The `id` is neither a URL nor a Base64-encoded string."); } } else { // println!("The 'id' field is not a string."); - return Value::Null; + return Err("The 'id' field is not a string."); } } else { // println!("The 'id' field does not exist."); - return Value::Null; + return Err("The 'id' field does not exist."); } if let Some(_image_content) = parsed_json["content"].as_str() { parsed_json["content"] = Value::String(encoded_string); } else { // println!("Key 'content' in 'image' not found."); + return Err("Key 'content' in 'image' not found."); } // Directly mutate the `contentType` value of the image @@ -330,7 +327,7 @@ pub fn image_to_elm_media_object(image_value: Value) -> Value { } Err(e) => { eprintln!("Error: {}", e); - Value::Null // Assign an empty string or a default value in case of an error + return Err("conten_type not found"); // Assign an empty string or a default value in case of an error } }; @@ -338,6 +335,7 @@ pub fn image_to_elm_media_object(image_value: Value) -> Value { parsed_json["contentType"] = encoded_content_type; } else { // println!("Key 'contentType' in 'image' not found."); + return Err("Key 'contentType' in 'image' not found.") } // Directly mutate the `encoding` value of the image @@ -346,9 +344,9 @@ pub fn image_to_elm_media_object(image_value: Value) -> Value { // println!("Successfully added encoding type to the image."); encoding_value // Assign the encoded string to the variable } - Err(e) => { - eprintln!("Error: {}", e); - Value::Null // Assign an empty string or a default value in case of an error + Err(_e) => { + //eprintln!("Error: {}", e); + return Err("encoding failed"); // Assign an empty string or a default value in case of an error } }; @@ -356,15 +354,14 @@ pub fn image_to_elm_media_object(image_value: Value) -> Value { parsed_json["contentEncoding"] = encoding_value; } else { // println!("Key 'contentEncoding' in 'image' not found."); + return Err("Key 'contentEncoding' in 'image' not found."); } -// println!("{:#?}", parsed_json); - parsed_json + // println!("{:#?}", parsed_json); + Ok(parsed_json) } - - -pub fn image_to_individual_display(image_value: Value) -> Value { +pub fn image_to_individual_display(image_value: Value) -> Result { //inspect the image object and re write it so it can be reused in ELM //we need to achieve the following structure into the indivudualDisplay array: @@ -400,11 +397,18 @@ pub fn image_to_individual_display(image_value: Value) -> Value { // OB usess the id field to point to an image or have the image encoded. // Content type is also based on either URL or encoding in the id. // Extract the `id` field - let image_object_value = image_to_elm_media_object(image_value); - if let Some(_image_data) = parsed_json["displayDetail"][0]["image"].as_object() { - parsed_json["displayDetail"][0]["image"] = image_object_value; - } else { - // println!("Key 'contentType' in 'image' not found."); + // let image_object_value = image_to_elm_media_object(image_value); + // if let Some(_image_data) = parsed_json["displayDetail"][0]["image"].as_object() { + // parsed_json["displayDetail"][0]["image"] = image_object_value; + // } else { + // // println!("Key 'contentType' in 'image' not found."); + // return Value::Null; + // } + + let result = image_to_elm_media_object(image_value); + match result { + Ok(image_object_value) => {parsed_json["displayDetail"][0]["image"] = image_object_value;} + Err(_err) => {return Err(_err);} } // Directly mutate the `language` value @@ -415,7 +419,7 @@ pub fn image_to_individual_display(image_value: Value) -> Value { } Err(e) => { eprintln!("Error: {}", e); - Value::Null // Assign an empty string or a default value in case of an error + return Err("language value not set properly"); // Assign an empty string or a default value in case of an error } }; @@ -423,13 +427,14 @@ pub fn image_to_individual_display(image_value: Value) -> Value { parsed_json["language"] = language_value; } else { // println!("Key 'language' in 'individualDisplay' not found."); + return Err("Key 'language' in 'individualDisplay' not found."); } //println!("{:#?}", parsed_json); - parsed_json + Ok(parsed_json) } -pub fn create_display_parameter(image_value: Value) -> Value { +pub fn create_display_parameter(image_value: Value) -> Result { //inspect the image object and re write it so it can be reused in ELM //we need to achieve the following structure into the indivudualDisplay array: @@ -478,9 +483,12 @@ pub fn create_display_parameter(image_value: Value) -> Value { // Add individual display value // Set the contentType to a choosen value (currently default to PNG) - parsed_dp_json["individualDisplay"] = Value::Array(vec![image_to_individual_display(image_value)]); - - parsed_dp_json + let result = image_to_individual_display(image_value); + match result { + Ok(individual_display_image) => {parsed_dp_json["individualDisplay"] = Value::Array(vec![individual_display_image]);} + Err(_err) => {return Err(_err)} + } + Ok(parsed_dp_json) // if let Some(id_value) = identity_value.get("identityHash") { // if identity_type.eq(&"Student ID".to_string()) { diff --git a/src/backend/candidate_value.rs b/src/backend/candidate_value.rs index 4a8a1fc..f43853b 100644 --- a/src/backend/candidate_value.rs +++ b/src/backend/candidate_value.rs @@ -24,7 +24,7 @@ pub fn set_candidate_output_value(state: &mut AppState, push_transformation: boo // todo: Can we we just get the key pointed to instead of copynig the entire repository? let mut temp_repository = state.repository.clone(); - temp_repository.apply_transformation(transformation.clone(), state.mapping); + let _ = temp_repository.apply_transformation(transformation.clone(), state.mapping); let candidate_output_value = temp_repository .get(&state.mapping.output_format()) diff --git a/src/backend/desm_mapping.rs b/src/backend/desm_mapping.rs index fe483de..3792ca8 100644 --- a/src/backend/desm_mapping.rs +++ b/src/backend/desm_mapping.rs @@ -29,13 +29,20 @@ pub fn apply_desm_mapping(state: &mut AppState) { trace_dbg!(&transformations); state.performed_mappings.extend(transformations.clone()); - let mut completed_fields = state.repository.apply_transformations(transformations, state.mapping); - for tuple in &mut completed_fields { - tuple.0 = tuple.0.trim_start_matches('$').replace('.', "/"); - tuple.1 = tuple.1.trim_start_matches('$').replace('.', "/"); + let result = state.repository.apply_transformations(transformations, state.mapping); + match result { + Ok(mut completed_fields) => { + for tuple in &mut completed_fields { + tuple.0 = tuple.0.trim_start_matches('$').replace('.', "/"); + tuple.1 = tuple.1.trim_start_matches('$').replace('.', "/"); + } + state.completed_fields.append(&mut completed_fields); + trace_dbg!(&state.completed_fields); + } + Err(_error) => { + //to handle + } } - state.completed_fields.append(&mut completed_fields); - trace_dbg!(&state.completed_fields); } pub fn desm_csv_parser(path: &str) -> Vec { diff --git a/src/backend/elm_mapping_helper.rs b/src/backend/elm_mapping_helper.rs index 500a214..ebe0a43 100644 --- a/src/backend/elm_mapping_helper.rs +++ b/src/backend/elm_mapping_helper.rs @@ -66,8 +66,6 @@ pub fn address_to_location(address_value: Value) -> Value { parsed_json } - - /// Creates specifiedBy based on input type in string found title /// /// # Arguments @@ -76,9 +74,9 @@ pub fn address_to_location(address_value: Value) -> Value { /// # Returns /// - Value: The content value Object in ELM format if successful. pub fn title_to_specifiedby(title: Value) -> Value { - //inspect the title object and re write it so it can be reused in ELM for building a Specification - //we need to achieve the following structure for a specification: - let json_data = r#" + //inspect the title object and re write it so it can be reused in ELM for building a Specification + //we need to achieve the following structure for a specification: + let json_data = r#" { "id": "urn:epass:learningAchievementSpec:1", "type": "Qualification", @@ -88,31 +86,24 @@ pub fn title_to_specifiedby(title: Value) -> Value { } "#; - let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); + let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); - // Directly mutate the `Location` value - // Access the "addressCountryCode" field - if let Some(title_str) = title.as_str() { - if title_str.is_empty() { - return Value::Null; - } - else{ + // Directly mutate the `Location` value + // Access the "addressCountryCode" field + if let Some(title_str) = title.as_str() { + if title_str.is_empty() { + return Value::Null; + } else { parsed_json["title"]["en"][0] = Value::String(title_str.to_string()); - - } - } else { + } + } else { return Value::Null; - } - + } - //println!("{:#?}", parsed_json); - parsed_json + //println!("{:#?}", parsed_json); + parsed_json } - - - - /// Creates specifiedBy based on input type in string found title /// /// # Arguments @@ -121,9 +112,9 @@ pub fn title_to_specifiedby(title: Value) -> Value { /// # Returns /// - Value: The creditpoint value Object in ELM format if successful. pub fn credentialpoint_values_to_object(credits: Value) -> Value { - //inspect the title object and re write it so it can be reused in ELM for building a creditpoint that cn be used in Specification - //we need to achieve the following structure for a creditpoint: - let json_data = r#" + //inspect the title object and re write it so it can be reused in ELM for building a creditpoint that cn be used in Specification + //we need to achieve the following structure for a creditpoint: + let json_data = r#" { "id": "urn:epass:creditPoint:1", "type": "CreditPoint", @@ -142,22 +133,25 @@ pub fn credentialpoint_values_to_object(credits: Value) -> Value { } "#; - let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); + let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); - // Directly mutate the `Location` value - // Access the "addressCountryCode" field - match credits { - Value::String(_) => {parsed_json["point"] = Value::String(credits.to_string());}, - Value::Number(_) => {parsed_json["point"] = Value::String(credits.to_string());}, - _ => {return Value::Null;} - } - //println!("{:#?}", parsed_json); - parsed_json + // Directly mutate the `Location` value + // Access the "addressCountryCode" field + match credits { + Value::String(_) => { + parsed_json["point"] = Value::String(credits.to_string()); + } + Value::Number(_) => { + parsed_json["point"] = Value::String(credits.to_string()); + } + _ => { + return Value::Null; + } + } + //println!("{:#?}", parsed_json); + parsed_json } - - - /// Creates specifiedBy based on input type in string found title /// /// # Arguments @@ -166,9 +160,9 @@ pub fn credentialpoint_values_to_object(credits: Value) -> Value { /// # Returns /// - Value: The content value Object in ELM format if successful. pub fn eqf_to_specifiedby_qualification(alignment: Value) -> Value { - //inspect the title object and re write it so it can be reused in ELM for building a creditpoint that cn be used in Specification - //we need to achieve the following structure for a creditpoint: - let json_data = r#" + //inspect the title object and re write it so it can be reused in ELM for building a creditpoint that cn be used in Specification + //we need to achieve the following structure for a creditpoint: + let json_data = r#" { "id": "http://data.europa.eu/snb/eqf/5", "type": "Concept", @@ -182,27 +176,30 @@ pub fn eqf_to_specifiedby_qualification(alignment: Value) -> Value { } "#; - let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); - println!("{:#?}", alignment); + let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); + //println!("{:#?}", alignment); // Extract the array from the Value if let Some(array) = alignment.as_array() { - // Find the targetCode where targetType == "ext:EQF" - if let Some(target_code) = array.iter() - .find(|item| item.get("targetType").and_then(|v| v.as_str()) == Some("ext:EQF")) - .and_then(|item| item.get("targetCode").and_then(|v| v.as_str())) - { - parsed_json["id"] = Value::String(format!("http://publications.europa.eu/resource/authority/language/{}", target_code)); - parsed_json["prefLabel"]["en"] = Value::String(format!("Level {}", target_code)); - } else { - println!("targetCode not found."); - return Value::Null; - } - } else { - println!("Error: Data is not an array."); - return Value::Null; - } - + // Find the targetCode where targetType == "ext:EQF" + if let Some(target_code) = array + .iter() + .find(|item| item.get("targetType").and_then(|v| v.as_str()) == Some("ext:EQF")) + .and_then(|item| item.get("targetCode").and_then(|v| v.as_str())) + { + parsed_json["id"] = Value::String(format!( + "http://publications.europa.eu/resource/authority/language/{}", + target_code + )); + parsed_json["prefLabel"]["en"] = Value::String(format!("Level {}", target_code)); + } else { + //println!("targetCode not found."); + return Value::Null; + } + } else { + //println!("Error: Data is not an array."); + return Value::Null; + } - //println!("{:#?}", parsed_json); - parsed_json -} \ No newline at end of file + //println!("{:#?}", parsed_json); + parsed_json +} diff --git a/src/backend/init_conversion.rs b/src/backend/init_conversion.rs index c4fb51b..f0d6061 100644 --- a/src/backend/init_conversion.rs +++ b/src/backend/init_conversion.rs @@ -79,11 +79,16 @@ pub fn load_mapping_file(state: &mut AppState) { } else { let rdr = std::fs::File::open(&state.mapping_path).unwrap(); let transformations: Vec = serde_json::from_reader(rdr).unwrap(); - trace_dbg!("Successfully loaded the mapping file"); - - state.repository.apply_transformations(transformations, state.mapping); + let result = state.repository.apply_transformations(transformations, state.mapping); + match result { + Ok(_value) => {} + Err(_error) => { + state.exit_warning = true; + } + } // todo: add applied transformation to completed fields + //println!("state: {:#?}", state.mapping); } } diff --git a/src/backend/repository copy.rs b/src/backend/repository copy.rs new file mode 100644 index 0000000..a7eeaac --- /dev/null +++ b/src/backend/repository copy.rs @@ -0,0 +1,1085 @@ +use crate::{ + backend::{ + base64_encode::{create_display_parameter,image_to_elm_media_object}, + elm_mapping_helper::{address_to_location, credentialpoint_values_to_object, eqf_to_specifiedby_qualification, title_to_specifiedby}, + jsonpointer::{JsonPath, JsonPointer}, + leaf_nodes::construct_leaf_node, + transformations::{DataLocation, DataTypeLocation, StringArrayValue, StringValue, Transformation}, + }, + state::{AppState, Mapping}, + trace_dbg, +}; +use jsonpath_rust::JsonPathFinder; +use serde_json::{json, Map, Value}; +//use tracing_subscriber::fmt::format; +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, +}; + +#[derive(Debug, Default, Clone)] +pub struct Repository(HashMap); + +impl DerefMut for Repository { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Deref for Repository { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for Repository { + fn from(map: HashMap) -> Self { + Self(map) + } +} + +impl Repository { + pub fn apply_transformation( + &mut self, + transformation: Transformation, + mapping: Mapping, + ) -> Option<(String, String)> { + match transformation { + Transformation::OneToOne { + type_: transformation, + source: + DataLocation { + format: source_format, + path: mut source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + // custom code to handle the special character '@' in the source_path + if source_path == "$.@context" { + source_path = r#"$["@context"]"#.to_string(); + }; + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(source_value); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + Transformation::ManyToOne { + type_: transformation, + sources, + destination, + } => { + if sources.iter().any(|source| source.format != mapping.input_format()) + || destination.format != mapping.output_format() + { + return None; + } + + let source_values = sources + .iter() + .map(|source| { + let source_credential = self.get(&source.format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source.path).unwrap(); + finder.find().as_array().unwrap().first().unwrap().clone() + }) + .collect::>(); + + let destination_credential = self.entry(destination.format).or_insert(json!({})); + let pointer = JsonPointer::try_from(JsonPath(destination.path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(source_values); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + None // Todo: this is not implemented yet, so returns None for now + } + + Transformation::StringToOne { + type_: transformation, + source: StringValue { value: source_value }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if destination_format != mapping.output_format() { + return None; + } + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path)).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(source_value); + } + + merge(destination_credential, leaf_node); + None + } + + Transformation::StringArrayToOne { + type_: transformation, + source: StringArrayValue { value: source_value }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if destination_format != mapping.output_format() { + return None; + } + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path)).unwrap(); + let mut leaf_node = construct_leaf_node(&pointer); + if let Some(value) = leaf_node.pointer_mut(&pointer) { + let json_value: Value = Value::Array( + source_value + .into_iter() + .map(Value::String) // Convert each String into serde_json::Value::String + .collect(), + ); + *value = transformation.apply(json_value); + } + + merge(destination_credential, leaf_node); + None + } + + Transformation::JsonToMarkdown { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + + // run the source value through a markdown converter to fit the nested objects into a markdown string + let markdown_source_value = json!(json_to_markdown(&source_value, 0)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(markdown_source_value); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + Transformation::MarkdownToJson { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + + if let Some(inner_string) = &source_value.as_str() { + let mut lines: Vec<&str> = inner_string.lines().collect(); + + lines.insert(0, ""); + + // Split the string by newlines and collect into Vec<&str> + let markdown_function_result = markdown_to_json(&lines); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(markdown_function_result); + } + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + Transformation::AddIdentifier { + type_: transformation, + source: + DataTypeLocation { + format: source_format, + datatype: source_type, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + let identifier_function_result = values_to_identity(&source_type, source_value); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(identifier_function_result); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + Transformation::IdentifierToObject { + type_: transformation, + source: + DataTypeLocation { + format: source_format, + datatype: source_type, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + let identifier_function_result = identity_to_object(&source_type, source_value); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(identifier_function_result); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + Transformation::ImageToIndividualDisplay { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + + // run the source value through a markdown converter to fit the nested objects into a markdown string + // let image_individualdisplay_source = Value::Array(vec![json!(create_display_parameter(source_value))]); + let image_individualdisplay_source = json!(create_display_parameter(source_value)); + // let markdown_source_value = json!(image_to_individual_display(source_value)); + + + if image_individualdisplay_source.is_null(){ + //println!("{:#?}", image_individualdisplay_source); + } + if let Some(value) = leaf_node.pointer_mut(&pointer) { + + *value = transformation.apply(image_individualdisplay_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + Transformation::ImageToMediaObject { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + + // run the source value through a markdown converter to fit the nested objects into a markdown string + // let image_individualdisplay_source = Value::Array(vec![json!(create_display_parameter(source_value))]); + let image_individualdisplay_source = json!(image_to_elm_media_object(source_value)); + if image_individualdisplay_source.is_null() { + return None; + } + + // let markdown_source_value = json!(image_to_individual_display(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(image_individualdisplay_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + + Transformation::TitleToSpecifiedByObject { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + // run the source value through a speficfiedby converter to fit the nested objects into a markdown string + let specifiedby_source = json!(title_to_specifiedby(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(specifiedby_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + Transformation::CreditToSpecifiedByObject { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + // run the source value through a speficfiedby converter to fit the nested objects into a markdown string + let cred_specifiedby_source = json!(credentialpoint_values_to_object(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(cred_specifiedby_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + Transformation::EqfToSpecifiedByQualification { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + // run the source value through a speficfiedby converter to fit the nested objects into a markdown string + let cred_specifiedby_source = json!(eqf_to_specifiedby_qualification(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(cred_specifiedby_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + Transformation::AddressToLocation { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return None; + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return None; + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + // run the source value through a markdown converter to fit the nested objects into a markdown string + // let image_individualdisplay_source = Value::Array(vec![json!(create_display_parameter(source_value))]); + let location_source = json!(address_to_location(source_value)); + // let markdown_source_value = json!(image_to_individual_display(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(location_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Some((destination_path, source_path)) + } + + _ => todo!(), + } + } + + pub fn apply_transformations( + &mut self, + transformations: Vec, + mapping: Mapping, + ) -> Vec<(String, String)> { + let mut completed_fields: Vec<(String, String)> = Vec::new(); + + for transformation in transformations { + if let Some(completed_field) = self.apply_transformation(transformation, mapping) { + trace_dbg!(&completed_field); + completed_fields.push(completed_field); + } + } + + completed_fields + } + + pub fn clear_mapping(&mut self, mut output_pointer: String, mapping: Mapping) { + let output_json = self.get_mut(&mapping.output_format()).unwrap(); + + output_pointer = output_pointer.trim_start_matches("/").to_string(); + let keys: Vec = output_pointer.split('/').map(|s| s.to_string()).collect(); + + remove_key_recursive(output_json, &keys); + } +} + +pub fn merge(a: &mut Value, b: Value) { + match (a, b) { + (a @ &mut Value::Object(_), Value::Object(b)) => { + let a = a.as_object_mut().unwrap(); + for (k, v) in b { + merge(a.entry(k).or_insert(Value::Null), v); + } + } + (a @ &mut Value::Array(_), Value::Array(b_arr)) => { + let a_arr = a.as_array_mut().unwrap(); + let a_len = a_arr.len(); + let b_iter = b_arr.into_iter(); + + for (i, b_val) in b_iter.enumerate() { + if i < a_len { + merge(&mut a_arr[i], b_val); + } else { + a_arr.push(b_val); + } + } + } + (_, Value::Null) => { + // If the incoming merge Json Value is `Value::Null`, do nothing to the existing Json Value,the current repository, `a` + } + (a, b) => *a = b, + } +} + +pub fn update_repository(state: &mut AppState) { + let output_pointer = state.output_pointer.clone(); + let output_format = state.mapping.output_format(); + + let source_value = state.candidate_output_value.clone(); + let output_json = state.repository.get_mut(&output_format).unwrap(); + + let mut leaf_node = construct_leaf_node(&output_pointer); + + if let Some(value) = leaf_node.pointer_mut(&output_pointer) { + *value = serde_json::from_str(&source_value).unwrap(); + } + + merge(output_json, leaf_node); +} + +fn remove_key_recursive(current_json: &mut Value, keys: &[String]) -> bool { + if let Some((first, rest)) = keys.split_first() { + if let Some(obj) = current_json.as_object_mut() { + if rest.is_empty() { + obj.remove(first); + } else if let Some(child) = obj.get_mut(first) { + // Recursively traverse deeper layers + if remove_key_recursive(child, rest) { + // If the child is now empty, remove it + obj.remove(first); + } + } + + // Return true if the current object is empty + return obj.is_empty(); + } + } + + false +} + +fn values_to_identity(identity_type: &str, identity_value: Value) -> Value { + //Create a new identity object that is fit for puprose in OBv3 (so not to lose information) + + let mut new_object = Map::new(); + new_object.insert("type".to_string(), Value::String("IdentityObject".to_string())); + new_object.insert("identityHash".to_string(), identity_value); + new_object.insert("identityType".to_string(), Value::String(identity_type.to_string())); + new_object.insert("hashed".to_string(), Value::Bool(false)); + new_object.insert("salt".to_string(), Value::String("not-used".to_string())); + // let _current_value = Value::Object(new_object); + Value::Array(vec![Value::Object(new_object)]) +} + +fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { + //inspect the identity object and re write it so it can be reused in ELM + + //we need to achieve the followin structures: + // "identifier": [ + // { + // "id": "urn:epass:identifier:2", + // "type": "Identifier", + // "notation": "75541452", + // "schemeName": "Student ID" + // } + // ], + + // and for example + // "givenName": { + // "en": ["David"] + // }, + + if let Some(id_value) = identity_value.get("identityHash") { + if identity_type.eq(&"Student ID".to_string()) { + let mut new_object = Map::new(); + new_object.insert("id".to_string(), Value::String("urn:epass:identifier:2".to_string())); + new_object.insert("type".to_string(), Value::String("Identifier".to_string())); + new_object.insert("notation".to_string(), id_value.clone()); + new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); + // return the value arrray + Value::Array(vec![Value::Object(new_object)]) + } else { + id_value.clone() + } + } else { + Value::String("".to_string()) + } +} + +fn json_to_markdown(json: &Value, indent_level: usize) -> String { + let mut markdown = String::new(); + let indent = " ".repeat(indent_level); + + match json { + // Handle JSON objects (key-value pairs) + Value::Object(map) => { + for (key, value) in map { + // Add the key as a bold label + markdown.push_str(&format!("{}**{}**:\n", indent, key)); + // Recursively handle the value + markdown.push_str(&json_to_markdown(value, indent_level + 1)); + } + } + + // Handle JSON arrays + Value::Array(array) => { + for item in array { + // Add each array item as a list item + markdown.push_str(&format!("{}- ", indent)); + markdown.push_str(&json_to_markdown(item, indent_level + 1)); + } + } + + // Handle primitive types: strings, numbers, booleans, and null + Value::String(s) => markdown.push_str(&format!("{}{}\n", indent, s)), + Value::Number(n) => markdown.push_str(&format!("{}{}\n", indent, n)), + Value::Bool(b) => markdown.push_str(&format!("{}{}\n", indent, b)), + Value::Null => markdown.push_str(&format!("{}null\n", indent)), + } + + markdown +} + +fn markdown_to_json(lines: &[&str]) -> Value { + // Recursively converts indented lines of Markdown into a JSON structure. + // 1. Parsing Markdown: + // • Headings (#): These are treated as keys in the resulting JSON object. + // • Bold Text (**): This is also treated as a key in the JSON object. + // • List Items (-): These are treated as elements in a JSON array. + // • Plain Text: If it’s not part of a list or a key, it’s treated as a value associated with the last key in the current JSON object. + // 2. Indentation Handling: + // • The code tracks the current indentation level of the Markdown. If the indentation increases, it means a new nested structure (object or array) is starting. If it decreases, the last completed structure is attached to the parent object or array. + // 3. Stack Management: + // • A stack is used to manage the nested structure. Each time a new nested object or array is detected, it’s pushed onto the stack. Once the nesting ends (indentation decreases), the structure is popped from the stack and integrated into the parent structure. + // 4. Regex Patterns: + // • heading_regex: Matches Markdown headings (e.g., # Title). + // • bold_regex: Matches bolded keys (e.g., **Key**:). + // • list_item_regex: Matches list items (e.g., - item). + + let mut i = 0; + let mut position: Vec = Vec::new(); + + //lets create a string to which we will concatenate new lines based on the markdown lines + position.insert(0, "".to_string()); + let mut json_string = String::from(""); + // evaluate if the input contains markdown or not + // if markdown is detected we will try to create json otherwise the value of the "markdown" will be put straight into the attribute + if !lines.contains(&"**") { + while i < lines.len() { + let line = lines[i]; + json_string.push_str(line); + i += 1; + } + let parsed_json: Value = serde_json::to_value(&json_string).unwrap(); + parsed_json + } else { + while i < lines.len() { + let line = lines[i]; + + // Handle key-value pairs (e.g., **key**: value) + let (obj_type, depth) = evaluate_line(line); + if obj_type == "E" && i == 0 { + // open a json object + json_string.push_str("{\n"); + } + + if obj_type == "O" { + while depth < position.len() - 1 { + // we need to close the positions + if let Some(last_value) = position.last() { + if last_value == "O" { + // The last value is O we can now close this object + if let Some(_last_value) = position.pop() { + json_string.pop(); + json_string = json_string.trim_end_matches(',').to_string(); + json_string.push_str("},\n"); + } + } else if last_value == "A" { + // close value A + if let Some(_last_value) = position.pop() { + json_string.push_str("]\n"); + } + } else if last_value == "OA" && depth < position.len() - 1 { + //The last value is OA + if let Some(_last_value) = position.pop() { + // first close the array + json_string.pop(); + json_string = json_string.trim_end_matches(',').to_string(); + json_string.push_str("],\n"); + if depth > 0 { + if let Some(_last_value) = position.pop() { + // then close the object + json_string.push_str("},\n"); + } + } else { + //"The vector was empty, nothing to remove."); + } + } else { + // ("The vector was empty, nothing to remove."); + } + } else { + // we have a different last value we will remove it from tha vector + if let Some(_last_value) = position.pop() { + // json_string.push_str("]\n"); + } + } + } else { + // println!("The vector is empty."); + } + } + + // setup the vector array for the right value and position + if depth >= position.len() - 1 && i > 0 { + //test previous line to see is we might have a nested object + let (last_obj_type, _new_depth) = evaluate_line(lines[i - 1]); + if last_obj_type == "O" { + json_string.push('{'); + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + position.insert(depth, "O".to_string()); + } else if let Some(last_value) = position.last() { + if last_value == "OA" && depth == position.len() - 1 { + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + } else if depth > position.len() - 1 { + json_string.push('{'); + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + position.insert(depth, "O".to_string()); + } else { + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + position[depth] = "O".to_string(); + } + } + } + } else if obj_type == "A" { + while depth < position.len() - 1 { + // we need to close the positions + if let Some(last_value) = position.last() { + if last_value == "O" { + if let Some(_last_value) = position.pop() { + json_string.push_str("},\n"); + } + } else if last_value == "A" { + if let Some(_last_value) = position.pop() { + json_string.push_str("]\n"); + } + } else { + // The last value is something leave it + } + } else { + // The vector is empty. + } + } + + // setup the vector array for the right value and position + if depth >= position.len() - 1 { + if let Some(last_value) = position.last() { + if last_value == "OA" { + json_string.push('['); + } else if last_value == "A" && depth == position.len() - 1 { + position.insert(depth, "A".to_string()); + } else { + position.insert(depth, "A".to_string()); + json_string.push('['); + } + } + json_string.push_str(&cleanup_string(line)); + } + } else if obj_type == "OA" { + while depth < position.len() - 1 { + // we need to close the positions + if let Some(last_value) = position.last() { + if last_value == "O" { + if let Some(_last_value) = position.pop() { + json_string.pop(); + json_string.pop(); + json_string.push_str("},\n"); + } + } else if last_value == "A" { + if let Some(_last_value) = position.pop() { + json_string.push_str("]\n"); + } else { + // The vector was empty, nothing to remove. + } + } else { + // The last value is something else leave it + } + } else { + // The vector is empty + } + } + + // we are creating a new array that will contain objects of the same type + json_string.push_str("[ \n {"); + json_string.push_str(&cleanup_string(line)); + json_string.push(':'); + // test if extra handling is needed for closing + position.insert(depth - 1, "OA".to_string()); + position.insert(depth, "O".to_string()); + } else if obj_type == "V" { + json_string.push_str(&cleanup_string(line)); + json_string.push_str(",\n"); + } + + i += 1; + } + + // Finalize the string to which we will concatenate new lines based on the markdown lines + json_string.pop(); + json_string = json_string.trim_end_matches(',').to_string(); + json_string.push_str("\n}"); + let parsed_json: Value = serde_json::from_str(&json_string).unwrap(); + parsed_json + } +} + +fn evaluate_line(line_to_test: &str) -> (String, usize) { + //test depth + let mut depth = line_to_test.chars().take_while(|c| c.is_whitespace()).count() / 2; + //test type + let line_type; + let trimmed = line_to_test.trim(); + if line_to_test.is_empty() { + // Handle list as object items of previous depth + line_type = "E"; + } else if trimmed.starts_with("-") && trimmed.ends_with("**:") { + // Handle list as object items of previous depth + line_type = "OA"; + depth += 1; + } else if trimmed.starts_with("**") { + // Handle list as object items of previous depth + line_type = "O"; + } else if trimmed.starts_with("-") { + // Handle as array items of previous depth + line_type = "A"; + } else { + // Handle value of previous depth + line_type = "V"; + } + + (line_type.to_string(), depth) +} + +fn cleanup_string(string_to_clean: &str) -> String { + //trim the string + let string_to_clean1 = string_to_clean.trim(); + let string_to_clean2 = string_to_clean1.replace("-", ""); + let string_to_clean3 = string_to_clean2.trim(); + let string_to_clean4 = string_to_clean3.replace("**:", ""); + let cleaned_string = string_to_clean4.trim().trim_matches('*').to_string(); + // Add quotes around the cleaned string + format!("\"{}\"", cleaned_string) +} diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 5220ed6..f8573b2 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -1,7 +1,10 @@ use crate::{ backend::{ - base64_encode::{create_display_parameter,image_to_elm_media_object}, - elm_mapping_helper::{address_to_location, credentialpoint_values_to_object, eqf_to_specifiedby_qualification, title_to_specifiedby}, + base64_encode::{create_display_parameter, image_to_elm_media_object}, + elm_mapping_helper::{ + address_to_location, credentialpoint_values_to_object, eqf_to_specifiedby_qualification, + title_to_specifiedby, + }, jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, transformations::{DataLocation, DataTypeLocation, StringArrayValue, StringValue, Transformation}, @@ -45,7 +48,7 @@ impl Repository { &mut self, transformation: Transformation, mapping: Mapping, - ) -> Option<(String, String)> { + ) -> Result, &'static str> { match transformation { Transformation::OneToOne { type_: transformation, @@ -61,7 +64,7 @@ impl Repository { }, } => { if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; + return Ok(None); } let source_credential = self.get(&source_format).unwrap(); @@ -77,7 +80,7 @@ impl Repository { // todo: still need to investigate other find() return types Some(array) => array.first().unwrap().clone(), None => { - return None; + return Ok(None); } }; @@ -94,7 +97,7 @@ impl Repository { merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) + Ok(Some((destination_path, source_path))) } Transformation::ManyToOne { type_: transformation, @@ -104,7 +107,7 @@ impl Repository { if sources.iter().any(|source| source.format != mapping.input_format()) || destination.format != mapping.output_format() { - return None; + return Ok(None); } let source_values = sources @@ -129,7 +132,7 @@ impl Repository { merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); - None // Todo: this is not implemented yet, so returns None for now + Ok(None) // Todo: this is not implemented yet, so returns None for now } Transformation::StringToOne { @@ -142,7 +145,7 @@ impl Repository { }, } => { if destination_format != mapping.output_format() { - return None; + return Ok(None); } let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. @@ -154,7 +157,7 @@ impl Repository { } merge(destination_credential, leaf_node); - None + Ok(None) } Transformation::StringArrayToOne { @@ -167,7 +170,7 @@ impl Repository { }, } => { if destination_format != mapping.output_format() { - return None; + return Ok(None); } let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. @@ -184,7 +187,7 @@ impl Repository { } merge(destination_credential, leaf_node); - None + Ok(None) } Transformation::JsonToMarkdown { @@ -201,7 +204,7 @@ impl Repository { }, } => { if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; + return Ok(None); } let source_credential = self.get(&source_format).unwrap(); @@ -212,7 +215,7 @@ impl Repository { // todo: still need to investigate other find() return types Some(array) => array.first().unwrap().clone(), None => { - return None; + return Ok(None); } }; @@ -231,7 +234,7 @@ impl Repository { merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) + Ok(Some((destination_path, source_path))) } Transformation::MarkdownToJson { @@ -248,7 +251,7 @@ impl Repository { }, } => { if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; + return Ok(None); } let source_credential = self.get(&source_format).unwrap(); @@ -259,7 +262,7 @@ impl Repository { // todo: still need to investigate other find() return types Some(array) => array.first().unwrap().clone(), None => { - return None; + return Ok(None); } }; @@ -284,7 +287,7 @@ impl Repository { merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) + Ok(Some((destination_path, source_path))) } Transformation::AddIdentifier { @@ -302,7 +305,7 @@ impl Repository { }, } => { if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; + return Ok(None); } let source_credential = self.get(&source_format).unwrap(); @@ -313,7 +316,7 @@ impl Repository { // todo: still need to investigate other find() return types Some(array) => array.first().unwrap().clone(), None => { - return None; + return Ok(None); } }; @@ -330,7 +333,7 @@ impl Repository { merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) + Ok(Some((destination_path, source_path))) } Transformation::IdentifierToObject { @@ -348,7 +351,7 @@ impl Repository { }, } => { if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; + return Ok(None); } let source_credential = self.get(&source_format).unwrap(); @@ -359,7 +362,7 @@ impl Repository { // todo: still need to investigate other find() return types Some(array) => array.first().unwrap().clone(), None => { - return None; + return Ok(None); } }; @@ -376,7 +379,7 @@ impl Repository { merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) + Ok(Some((destination_path, source_path))) } Transformation::ImageToIndividualDisplay { @@ -393,7 +396,7 @@ impl Repository { }, } => { if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; + return Ok(None); } let source_credential = self.get(&source_format).unwrap(); @@ -404,7 +407,7 @@ impl Repository { // todo: still need to investigate other find() return types Some(array) => array.first().unwrap().clone(), None => { - return None; + return Ok(None); } }; @@ -415,17 +418,21 @@ impl Repository { // run the source value through a markdown converter to fit the nested objects into a markdown string // let image_individualdisplay_source = Value::Array(vec![json!(create_display_parameter(source_value))]); - let image_individualdisplay_source = json!(create_display_parameter(source_value)); - // let markdown_source_value = json!(image_to_individual_display(source_value)); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(image_individualdisplay_source); + let result = create_display_parameter(source_value); + match result { + Ok(image_individualdisplay_source) => { + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(image_individualdisplay_source); + } + } + Err(_error) => {return Err(_error);} } + merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) + Ok(Some((destination_path, source_path))) } Transformation::ImageToMediaObject { @@ -442,7 +449,7 @@ impl Repository { }, } => { if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; + return Ok(None); } let source_credential = self.get(&source_format).unwrap(); @@ -453,7 +460,7 @@ impl Repository { // todo: still need to investigate other find() return types Some(array) => array.first().unwrap().clone(), None => { - return None; + return Ok(None); } }; @@ -464,24 +471,22 @@ impl Repository { // run the source value through a markdown converter to fit the nested objects into a markdown string // let image_individualdisplay_source = Value::Array(vec![json!(create_display_parameter(source_value))]); - let image_individualdisplay_source = json!(image_to_elm_media_object(source_value)); - if image_individualdisplay_source.is_null() { - return None; - } - - // let markdown_source_value = json!(image_to_individual_display(source_value)); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(image_individualdisplay_source); + let result = image_to_elm_media_object(source_value); + match result { + Ok(image_media_object_source) => { + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(image_media_object_source); + } + } + Err(_error) => {return Err(_error);} } merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) + Ok(Some((destination_path, source_path))) } - Transformation::TitleToSpecifiedByObject { type_: transformation, source: @@ -496,7 +501,7 @@ impl Repository { }, } => { if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; + return Ok(None); } let source_credential = self.get(&source_format).unwrap(); @@ -507,7 +512,7 @@ impl Repository { // todo: still need to investigate other find() return types Some(array) => array.first().unwrap().clone(), None => { - return None; + return Ok(None); } }; @@ -525,7 +530,7 @@ impl Repository { merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) + Ok(Some((destination_path, source_path))) } Transformation::CreditToSpecifiedByObject { @@ -542,7 +547,7 @@ impl Repository { }, } => { if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; + return Ok(None); } let source_credential = self.get(&source_format).unwrap(); @@ -553,7 +558,7 @@ impl Repository { // todo: still need to investigate other find() return types Some(array) => array.first().unwrap().clone(), None => { - return None; + return Ok(None); } }; @@ -571,7 +576,7 @@ impl Repository { merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) + Ok(Some((destination_path, source_path))) } Transformation::EqfToSpecifiedByQualification { @@ -588,7 +593,7 @@ impl Repository { }, } => { if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; + return Ok(None); } let source_credential = self.get(&source_format).unwrap(); @@ -599,7 +604,7 @@ impl Repository { // todo: still need to investigate other find() return types Some(array) => array.first().unwrap().clone(), None => { - return None; + return Ok(None); } }; @@ -617,7 +622,7 @@ impl Repository { merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) + Ok(Some((destination_path, source_path))) } Transformation::AddressToLocation { @@ -634,7 +639,7 @@ impl Repository { }, } => { if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; + return Ok(None); } let source_credential = self.get(&source_format).unwrap(); @@ -645,7 +650,7 @@ impl Repository { // todo: still need to investigate other find() return types Some(array) => array.first().unwrap().clone(), None => { - return None; + return Ok(None); } }; @@ -665,7 +670,7 @@ impl Repository { merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) + Ok(Some((destination_path, source_path))) } _ => todo!(), @@ -676,17 +681,21 @@ impl Repository { &mut self, transformations: Vec, mapping: Mapping, - ) -> Vec<(String, String)> { + ) -> Result, &'static str> { let mut completed_fields: Vec<(String, String)> = Vec::new(); - for transformation in transformations { - if let Some(completed_field) = self.apply_transformation(transformation, mapping) { - trace_dbg!(&completed_field); - completed_fields.push(completed_field); + let result = self.apply_transformation(transformation, mapping); + match result { + Ok(Some(completed_field)) => { + completed_fields.push(completed_field); + } + Ok(None) => {} // to do and check //println!("Ok(none)"); + Err(_error) => { + return Err(_error);} } } - completed_fields + Ok(completed_fields) } pub fn clear_mapping(&mut self, mut output_pointer: String, mapping: Mapping) { diff --git a/src/backend/routes/api.rs b/src/backend/routes/api.rs index 82c1e27..5fbc911 100644 --- a/src/backend/routes/api.rs +++ b/src/backend/routes/api.rs @@ -6,9 +6,9 @@ use axum::{ Json, }; //use config::Value; -use serde_json::{Value,json}; +use serde_json::{json, Value}; -use crate::backend::base64_encode::{decode_json,encode_json_file}; +use crate::backend::base64_encode::{decode_json, encode_json_file}; use crate::backend::headless_cli::load_files_apply_transformations; use crate::state::{AppState, Mapping}; use std::{fs::File, io::Write, path::Path}; @@ -33,10 +33,10 @@ pub async fn api(Json(input_json): Json) -> impl IntoResponse { let output_dir = "outputs"; let file_name = "export_file"; let _ = fs::create_dir_all(upload_dir).await.map_err(|_| { - let error_json = json!({ + let error_json = json!({ "error": "Internal Server Error", "message" : "Failed to create upload directory"}); - (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)) + (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)) }); let _ = fs::create_dir_all(output_dir).await.map_err(|_| { let error_json = json!({ @@ -59,41 +59,44 @@ pub async fn api(Json(input_json): Json) -> impl IntoResponse { _mapping_type = Mapping::ELMToOBv3; } Some(value) => { - let error_json = json!({ + let error_json = json!({ "error": "Bad Request", "message" : format!("Invalid translation value: {}", value)}); - return (StatusCode::BAD_REQUEST, Json(error_json)) + return (StatusCode::BAD_REQUEST, Json(error_json)); } None => { let error_json = json!({ "error": "Bad Request", "message" : "Invalid translation value: no key found"}); - return (StatusCode::BAD_REQUEST, Json(error_json)) + return (StatusCode::BAD_REQUEST, Json(error_json)); } } match input_json.get("Content").and_then(|v| v.as_str()) { Some(value) => { _input_file_path = format!("{}/{}", upload_dir, file_name); - let file = File::create(&_input_file_path) - .map_err(|_| { - let error_json = json!({ + let file = File::create(&_input_file_path).map_err(|_| { + let error_json = json!({ "error": "Internal Server Error", "message" : "Failed to create file"}); - (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)) - }); + (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)) + }); match decode_json(value) { Ok(data) => { match file { - Ok(mut file_found) => {let _ = file_found.write_all(&data);} - Err(file_error) => {return file_error;} - // Err(file_error) => {let error_json = json!({ - // "error": "Internal Server Error", - // "message" : "Failed to write to file"}); - // return (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json));} + Ok(mut file_found) => { + let _ = file_found.write_all(&data); + } + Err(file_error) => { + return file_error; + } // Err(file_error) => {let error_json = json!({ + // "error": "Internal Server Error", + // "message" : "Failed to write to file"}); + // return (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json));} } } - Err(_decode_err) => { let error_json = json!({ + Err(_decode_err) => { + let error_json = json!({ "error": "Internal Server Error", "message" : "Failed to read file data"}); return (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)); @@ -102,11 +105,11 @@ pub async fn api(Json(input_json): Json) -> impl IntoResponse { } None => { - let error_json = json!({ + let error_json = json!({ "error": "Bad Request", "message" : "Invalid data value: no key found"}); - return (StatusCode::BAD_REQUEST, Json(error_json)) - } + return (StatusCode::BAD_REQUEST, Json(error_json)); + } } // Define the output file path @@ -139,7 +142,7 @@ pub async fn api(Json(input_json): Json) -> impl IntoResponse { "message" : "Failed to read output file"}); (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)) }); - let _ =fs::remove_file(state.input_path).await.map_err(|_| { + let _ = fs::remove_file(state.input_path).await.map_err(|_| { let error_json = json!({ "error": "Internal Server Error", "message" : "Failed to remove input file"}); @@ -160,22 +163,32 @@ pub async fn api(Json(input_json): Json) -> impl IntoResponse { HeaderValue::from_static("application/application/json"), ); + println!("state_exitwarning: {:#?}", state.exit_warning); + match state.exit_warning { + true => { + let error_json = json!({ + "error": "Internal Server Error", + "message" : "Failed to encode the json file"}); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)); + } + false => {} + } + match output_file { - Ok(content) => {match encode_json_file(content) { - Ok(encoded_json) => { + Ok(content) => match encode_json_file(content) { + Ok(encoded_json) => { let response_json = json!({"content": encoded_json}); return (StatusCode::OK, Json(response_json)); } - Err (_enc_error) => { + Err(_enc_error) => { let error_json = json!({ "error": "Internal Server Error", "message" : "Failed to encode the json file"}); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)); } - } - } + }, Err(status) => { - return status; + return status; } } // let encoded_json = encode_json_file(output_file)?; diff --git a/src/backend/routes/translate_file.rs b/src/backend/routes/translate_file.rs index 6177e3f..280ee50 100644 --- a/src/backend/routes/translate_file.rs +++ b/src/backend/routes/translate_file.rs @@ -130,6 +130,14 @@ pub async fn translate_file(mut multipart: Multipart) -> Result Result {parsed_json["displayDetail"][0]["image"] = image_object_value;} - Err(_err) => {return Err(_err);} + Ok(image_object_value) => { + parsed_json["displayDetail"][0]["image"] = image_object_value; + } + Err(_err) => { + return Err(_err); + } } // Directly mutate the `language` value @@ -485,8 +489,10 @@ pub fn create_display_parameter(image_value: Value) -> Result {parsed_dp_json["individualDisplay"] = Value::Array(vec![individual_display_image]);} - Err(_err) => {return Err(_err)} + Ok(individual_display_image) => { + parsed_dp_json["individualDisplay"] = Value::Array(vec![individual_display_image]); + } + Err(_err) => return Err(_err), } Ok(parsed_dp_json) diff --git a/src/backend/repository.rs b/src/backend/repository.rs index f8573b2..d5167d7 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -425,10 +425,11 @@ impl Repository { *value = transformation.apply(image_individualdisplay_source); } } - Err(_error) => {return Err(_error);} + Err(_error) => { + return Err(_error); + } } - merge(destination_credential, leaf_node); trace_dbg!("Successfully completed transformation"); @@ -478,7 +479,9 @@ impl Repository { *value = transformation.apply(image_media_object_source); } } - Err(_error) => {return Err(_error);} + Err(_error) => { + return Err(_error); + } } merge(destination_credential, leaf_node); @@ -689,9 +692,10 @@ impl Repository { Ok(Some(completed_field)) => { completed_fields.push(completed_field); } - Ok(None) => {} // to do and check //println!("Ok(none)"); + Ok(None) => {} // to do and check //println!("Ok(none)"); Err(_error) => { - return Err(_error);} + return Err(_error); + } } } diff --git a/src/backend/routes/api.rs b/src/backend/routes/api.rs index 5fbc911..a0fccdb 100644 --- a/src/backend/routes/api.rs +++ b/src/backend/routes/api.rs @@ -163,7 +163,7 @@ pub async fn api(Json(input_json): Json) -> impl IntoResponse { HeaderValue::from_static("application/application/json"), ); - println!("state_exitwarning: {:#?}", state.exit_warning); + //println!("state_exitwarning: {:#?}", state.exit_warning); match state.exit_warning { true => { let error_json = json!({ @@ -178,17 +178,17 @@ pub async fn api(Json(input_json): Json) -> impl IntoResponse { Ok(content) => match encode_json_file(content) { Ok(encoded_json) => { let response_json = json!({"content": encoded_json}); - return (StatusCode::OK, Json(response_json)); + (StatusCode::OK, Json(response_json)) } Err(_enc_error) => { let error_json = json!({ "error": "Internal Server Error", "message" : "Failed to encode the json file"}); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)); + (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)) } }, Err(status) => { - return status; + status } } // let encoded_json = encode_json_file(output_file)?; diff --git a/src/backend/routes/translate_file.rs b/src/backend/routes/translate_file.rs index 280ee50..1ec91f7 100644 --- a/src/backend/routes/translate_file.rs +++ b/src/backend/routes/translate_file.rs @@ -131,12 +131,16 @@ pub async fn translate_file(mut multipart: Multipart) -> Result Date: Sat, 8 Feb 2025 01:46:40 +0100 Subject: [PATCH 41/45] Add learningoutcome transformation --- .../examples/DigiComp_Generic.json | 2 +- .../custom_mapping_OBv3_ELM_latest.json | 85 +- src/backend/base64_encode.rs | 4 +- src/backend/elm_mapping_helper.rs | 204 +++- src/backend/repository copy.rs | 1085 ----------------- src/backend/repository.rs | 183 ++- src/backend/routes/api.rs | 41 +- src/backend/routes/translate_file.rs | 17 +- src/backend/transformations.rs | 76 ++ test/uvh_at_microcredential_full_1.json | 20 + 10 files changed, 594 insertions(+), 1123 deletions(-) delete mode 100644 src/backend/repository copy.rs diff --git a/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic.json b/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic.json index 620a697..58b73f3 100644 --- a/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic.json +++ b/json/ebsi-elm/vcdm2.0-europass-edc-schema/examples/DigiComp_Generic.json @@ -452,7 +452,7 @@ "type": "Note", "noteLiteral": { "en": [ - "- Description of DigiCompCompetence\n- Description of DigiCompCompetence 2" + "De student heeft de eerste stap van transmutatie afgerond, waarbij katten zonder blijvende schade worden omgezet in keukenapparatuur." ] } }, diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index 3db1b21..5c71cb6 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -139,6 +139,30 @@ "path": "$.credentialSubject.type" } }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "Student ID", + "path": "$.credentialSubject.identifier[0]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.identifier" + } + }, + { + "type_": "identifierToObject", + "source": { + "format": "OBv3", + "datatype": "ext:fullName", + "path": "$.credentialSubject.identifier[0]" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.fullName.en[0]" + } + }, { "type_": "imageToIndividualDisplay", "source": { @@ -291,27 +315,72 @@ } }, { - "type_": "identifierToObject", + "type_": "translateLearningOutcome", "source": { "format": "OBv3", - "datatype": "Student ID", - "path": "$.credentialSubject.identifier[0]" + "path": "$.credentialSubject.achievement.learningOutcome" }, "destination": { "format": "ELM", - "path": "$.credentialSubject.identifier" + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" } }, + + + { - "type_": "identifierToObject", + "type_": "assessmentToProvenBy", "source": { "format": "OBv3", - "datatype": "ext:fullName", - "path": "$.credentialSubject.identifier[0]" + "path": "$.credentialSubject.achievement.assessmentType" }, "destination": { "format": "ELM", - "path": "$.credentialSubject.fullName.en[0]" + "path": "$.credentialSubject.hasClaim[0].provenBy[0]" + } + }, + { + "type_": "addressToLocation", + "source": { + "format": "OBv3", + "path": "$.issuer.address" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].provenBy[0].awardedBy.awardingBody[0].location" + } + }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.issuer.name" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].provenBy[0].awardedBy.awardingBody[0].legalName.en[0]" + } + }, + { + "type_": "objectToNoteLiteral", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.result" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].provenBy[0].grade.noteLiteral.en[0]" + } + }, + { + "type_": "createLearningOutcomeSummary", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.criteria.narrative" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcomeSummary" } } ] \ No newline at end of file diff --git a/src/backend/base64_encode.rs b/src/backend/base64_encode.rs index d09f2da..742cf3d 100644 --- a/src/backend/base64_encode.rs +++ b/src/backend/base64_encode.rs @@ -58,9 +58,7 @@ fn encode_image_from_url(url: &str) -> Result> { base64_string = Base64Engine.encode(&bytes); Ok(base64_string) } - Err(e) => { - Err(Box::new(e)) - } + Err(e) => Err(Box::new(e)), } } diff --git a/src/backend/elm_mapping_helper.rs b/src/backend/elm_mapping_helper.rs index ebe0a43..cd3447a 100644 --- a/src/backend/elm_mapping_helper.rs +++ b/src/backend/elm_mapping_helper.rs @@ -1,5 +1,5 @@ use codes_iso_3166::part_1::CountryCode; -use serde_json::Value; +use serde_json::{json, Value}; use std::str::FromStr; /// Creates country code based on input type in string found in addressCountryCode @@ -203,3 +203,205 @@ pub fn eqf_to_specifiedby_qualification(alignment: Value) -> Value { //println!("{:#?}", parsed_json); parsed_json } + +/// Creates specifiedBy based on input type in string found title +/// +/// # Arguments +/// - `assessment_type`: an array that could be found in OBv3 but needs to be translated to fit the new structure of ELM. +/// +/// # Returns +/// - Value: The specifiedBy object with dummies in ELM format if successful. +pub fn assessment_type_to_specifiedby_assesment(assessement_type: Value) -> Value { + //inspect the title object and re write it so it can be reused in ELM for building a creditpoint that cn be used in Specification + //we need to achieve the following structure for a creditpoint: + let json_data = r#" +{ + "id": "urn:epass:learningAssessment:1", + "type": "LearningAssessment", + "awardedBy": { + "id": "urn:epass:awardingProcess:1", + "type": "AwardingProcess", + "awardingBody": [ + { + "id": "urn:epass:org:1", + "type": "Organisation", + "location": [ {"address":"placeholder"} + ], + "legalName": { + "en": ["University of Life"] + } + } + ] + }, + "title": { + "en": ["AssessmentTypeValue"] + }, + "grade": { + "id": "urn:epass:note:1", + "type": "Note", + "noteLiteral": { + "en": ["0"] + } + } +} + +"#; + + let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); + + // Directly mutate the title value of the assessment + if let Some(assessement_type_str) = assessement_type.as_str() { + if assessement_type_str.is_empty() { + return Value::Null; + } else { + parsed_json["title"]["en"][0] = Value::String(assessement_type_str.to_string()); + } + } else { + return Value::Null; + } + + //println!("{:#?}", parsed_json); + parsed_json +} + +/// Creates specifiedBy based on input type in string found title +/// +/// # Arguments +/// - `assessment_type`: an array that could be found in OBv3 but needs to be translated to fit the new structure of ELM. +/// +/// # Returns +/// - Value: The specifiedBy object with dummies in ELM format if successful. +pub fn object_to_note_literal(any_object: Value) -> Value { + //inspect the title object and re write it so it can be reused in ELM for building a creditpoint that cn be used in Specification + //we need to achieve the following structure for a creditpoint: + + let str_array = handle_json_input(&any_object); + Value::String(str_array) +} + +/// Creates learningOutcomes based on input type in outcome array +/// +/// # Arguments +/// - `learning_outcomes`: an array that could be found in OBv3 but needs to be translated to fit the new structure of ELM. +/// +/// # Returns +/// - Value: The specifiedBy object with dummies in ELM format if successful. +pub fn transform_learning_outcomes(json_obj: Value) -> Value { + if let Some(learning_outcomes) = json_obj.as_array() { + let transformed_outcomes: Vec = learning_outcomes + .iter() + .map(|outcome| { + let new_related_skill = outcome.get("relatedSkill").map(|related| { + json!({ + "id": related.get("id").unwrap_or(&json!("")), + "type": "Concept", + "inScheme": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": [related.get("title").unwrap_or(&json!("")).as_str().unwrap_or("")] + } + }) + }); + + let mut new_outcome = outcome.clone(); + if let Some(obj) = new_outcome.as_object_mut() { + if let Some(new_related_skill) = new_related_skill { + obj.insert("relatedSkill".to_string(), new_related_skill); + } + obj.insert("title".to_string(), json!({"en": [outcome.get("title")]})); + } + // Modify fields inside the object + new_outcome + }) + .collect(); + + Value::Array(transformed_outcomes) + } else { + Value::Null + } +} + +/// Creates a learning outcomes summary structure +/// +/// # Arguments +/// - `criteria`: Tekst as found in criteria in OBv3. +/// +/// # Returns +/// - Value: The learningOutcomeSummary object with dummies in ELM format if successful. +pub fn create_learning_outcome_summary(json_obj: Value) -> Value { + if let Some(outcome_sum_str) = json_obj.as_str() { + if outcome_sum_str.is_empty() { + Value::Null + } else { + let json_result = json!({ + "id": "urn:epass:note:3", + "type": "Note", + "noteLiteral": { + "en": [outcome_sum_str] + } + }); + json_result + } + } else { + Value::Null + } +} + +// additional private helpers +// Function to handle both single object and array of objects +fn handle_json_input(json_obj: &Value) -> String { + if json_obj.is_array() { + // If it's an array, map each object to a string and join them + json_obj + .as_array() + .unwrap() + .iter() + .map(|obj| object_to_string(obj)) + .collect::>() + .join(" | ") // Separate objects with " | " + } else if json_obj.is_object() { + // If it's a single object, convert it directly + object_to_string(json_obj) + } else { + "Invalid JSON format".to_string() + } +} + +// Function to convert JSON object (with 1-level nesting) to a "key:value" string +fn object_to_string(json_obj: &Value) -> String { + if let Some(obj) = json_obj.as_object() { + obj.iter() + .flat_map(|(key, value)| { + match value { + Value::Object(nested_obj) => { + // Flatten nested objects: "key.nested_key:value" + nested_obj + .iter() + .map(|(nested_key, nested_value)| { + format!("{}.{}:{}", key, nested_key, value_to_string(nested_value)) + }) + .collect::>() + } + _ => vec![format!("{}:{}", key, value_to_string(value))], // Normal key:value + } + }) + .collect::>() + .join(", ") + } else { + "Invalid JSON object".to_string() + } +} + +// Converts JSON values to strings +fn value_to_string(value: &Value) -> String { + match value { + Value::String(s) => s.clone(), + Value::Number(n) => n.to_string(), + Value::Bool(b) => b.to_string(), + Value::Array(arr) => format!("[{}]", arr.iter().map(value_to_string).collect::>().join(", ")), + Value::Object(_) => "{...}".to_string(), // Shouldn't reach here due to flattening + Value::Null => "null".to_string(), + } +} diff --git a/src/backend/repository copy.rs b/src/backend/repository copy.rs deleted file mode 100644 index a7eeaac..0000000 --- a/src/backend/repository copy.rs +++ /dev/null @@ -1,1085 +0,0 @@ -use crate::{ - backend::{ - base64_encode::{create_display_parameter,image_to_elm_media_object}, - elm_mapping_helper::{address_to_location, credentialpoint_values_to_object, eqf_to_specifiedby_qualification, title_to_specifiedby}, - jsonpointer::{JsonPath, JsonPointer}, - leaf_nodes::construct_leaf_node, - transformations::{DataLocation, DataTypeLocation, StringArrayValue, StringValue, Transformation}, - }, - state::{AppState, Mapping}, - trace_dbg, -}; -use jsonpath_rust::JsonPathFinder; -use serde_json::{json, Map, Value}; -//use tracing_subscriber::fmt::format; -use std::{ - collections::HashMap, - ops::{Deref, DerefMut}, -}; - -#[derive(Debug, Default, Clone)] -pub struct Repository(HashMap); - -impl DerefMut for Repository { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Deref for Repository { - type Target = HashMap; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl From> for Repository { - fn from(map: HashMap) -> Self { - Self(map) - } -} - -impl Repository { - pub fn apply_transformation( - &mut self, - transformation: Transformation, - mapping: Mapping, - ) -> Option<(String, String)> { - match transformation { - Transformation::OneToOne { - type_: transformation, - source: - DataLocation { - format: source_format, - path: mut source_path, - }, - destination: - DataLocation { - format: destination_format, - path: destination_path, - }, - } => { - if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; - } - - let source_credential = self.get(&source_format).unwrap(); - - // custom code to handle the special character '@' in the source_path - if source_path == "$.@context" { - source_path = r#"$["@context"]"#.to_string(); - }; - - let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); - - let source_value = match finder.find().as_array() { - // todo: still need to investigate other find() return types - Some(array) => array.first().unwrap().clone(), - None => { - return None; - } - }; - - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. - - let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); - - let mut leaf_node = construct_leaf_node(&pointer); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(source_value); - } - - merge(destination_credential, leaf_node); - - trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) - } - Transformation::ManyToOne { - type_: transformation, - sources, - destination, - } => { - if sources.iter().any(|source| source.format != mapping.input_format()) - || destination.format != mapping.output_format() - { - return None; - } - - let source_values = sources - .iter() - .map(|source| { - let source_credential = self.get(&source.format).unwrap(); - - let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source.path).unwrap(); - finder.find().as_array().unwrap().first().unwrap().clone() - }) - .collect::>(); - - let destination_credential = self.entry(destination.format).or_insert(json!({})); - let pointer = JsonPointer::try_from(JsonPath(destination.path.clone())).unwrap(); - - let mut leaf_node = construct_leaf_node(&pointer); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(source_values); - } - - merge(destination_credential, leaf_node); - - trace_dbg!("Successfully completed transformation"); - None // Todo: this is not implemented yet, so returns None for now - } - - Transformation::StringToOne { - type_: transformation, - source: StringValue { value: source_value }, - destination: - DataLocation { - format: destination_format, - path: destination_path, - }, - } => { - if destination_format != mapping.output_format() { - return None; - } - - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. - let pointer = JsonPointer::try_from(JsonPath(destination_path)).unwrap(); - - let mut leaf_node = construct_leaf_node(&pointer); - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(source_value); - } - - merge(destination_credential, leaf_node); - None - } - - Transformation::StringArrayToOne { - type_: transformation, - source: StringArrayValue { value: source_value }, - destination: - DataLocation { - format: destination_format, - path: destination_path, - }, - } => { - if destination_format != mapping.output_format() { - return None; - } - - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. - let pointer = JsonPointer::try_from(JsonPath(destination_path)).unwrap(); - let mut leaf_node = construct_leaf_node(&pointer); - if let Some(value) = leaf_node.pointer_mut(&pointer) { - let json_value: Value = Value::Array( - source_value - .into_iter() - .map(Value::String) // Convert each String into serde_json::Value::String - .collect(), - ); - *value = transformation.apply(json_value); - } - - merge(destination_credential, leaf_node); - None - } - - Transformation::JsonToMarkdown { - type_: transformation, - source: - DataLocation { - format: source_format, - path: source_path, - }, - destination: - DataLocation { - format: destination_format, - path: destination_path, - }, - } => { - if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; - } - - let source_credential = self.get(&source_format).unwrap(); - - let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); - - let source_value = match finder.find().as_array() { - // todo: still need to investigate other find() return types - Some(array) => array.first().unwrap().clone(), - None => { - return None; - } - }; - - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. - let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); - - let mut leaf_node = construct_leaf_node(&pointer); - - // run the source value through a markdown converter to fit the nested objects into a markdown string - let markdown_source_value = json!(json_to_markdown(&source_value, 0)); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(markdown_source_value); - } - - merge(destination_credential, leaf_node); - - trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) - } - - Transformation::MarkdownToJson { - type_: transformation, - source: - DataLocation { - format: source_format, - path: source_path, - }, - destination: - DataLocation { - format: destination_format, - path: destination_path, - }, - } => { - if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; - } - - let source_credential = self.get(&source_format).unwrap(); - - let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); - - let source_value = match finder.find().as_array() { - // todo: still need to investigate other find() return types - Some(array) => array.first().unwrap().clone(), - None => { - return None; - } - }; - - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. - let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); - - let mut leaf_node = construct_leaf_node(&pointer); - - if let Some(inner_string) = &source_value.as_str() { - let mut lines: Vec<&str> = inner_string.lines().collect(); - - lines.insert(0, ""); - - // Split the string by newlines and collect into Vec<&str> - let markdown_function_result = markdown_to_json(&lines); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(markdown_function_result); - } - } - - merge(destination_credential, leaf_node); - - trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) - } - - Transformation::AddIdentifier { - type_: transformation, - source: - DataTypeLocation { - format: source_format, - datatype: source_type, - path: source_path, - }, - destination: - DataLocation { - format: destination_format, - path: destination_path, - }, - } => { - if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; - } - - let source_credential = self.get(&source_format).unwrap(); - - let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); - - let source_value = match finder.find().as_array() { - // todo: still need to investigate other find() return types - Some(array) => array.first().unwrap().clone(), - None => { - return None; - } - }; - - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. - let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); - - let mut leaf_node = construct_leaf_node(&pointer); - let identifier_function_result = values_to_identity(&source_type, source_value); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(identifier_function_result); - } - - merge(destination_credential, leaf_node); - - trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) - } - - Transformation::IdentifierToObject { - type_: transformation, - source: - DataTypeLocation { - format: source_format, - datatype: source_type, - path: source_path, - }, - destination: - DataLocation { - format: destination_format, - path: destination_path, - }, - } => { - if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; - } - - let source_credential = self.get(&source_format).unwrap(); - - let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); - - let source_value = match finder.find().as_array() { - // todo: still need to investigate other find() return types - Some(array) => array.first().unwrap().clone(), - None => { - return None; - } - }; - - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. - let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); - - let mut leaf_node = construct_leaf_node(&pointer); - let identifier_function_result = identity_to_object(&source_type, source_value); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(identifier_function_result); - } - - merge(destination_credential, leaf_node); - - trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) - } - - Transformation::ImageToIndividualDisplay { - type_: transformation, - source: - DataLocation { - format: source_format, - path: source_path, - }, - destination: - DataLocation { - format: destination_format, - path: destination_path, - }, - } => { - if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; - } - - let source_credential = self.get(&source_format).unwrap(); - - let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); - - let source_value = match finder.find().as_array() { - // todo: still need to investigate other find() return types - Some(array) => array.first().unwrap().clone(), - None => { - return None; - } - }; - - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. - let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); - - let mut leaf_node = construct_leaf_node(&pointer); - - // run the source value through a markdown converter to fit the nested objects into a markdown string - // let image_individualdisplay_source = Value::Array(vec![json!(create_display_parameter(source_value))]); - let image_individualdisplay_source = json!(create_display_parameter(source_value)); - // let markdown_source_value = json!(image_to_individual_display(source_value)); - - - if image_individualdisplay_source.is_null(){ - //println!("{:#?}", image_individualdisplay_source); - } - if let Some(value) = leaf_node.pointer_mut(&pointer) { - - *value = transformation.apply(image_individualdisplay_source); - } - - merge(destination_credential, leaf_node); - - trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) - } - - Transformation::ImageToMediaObject { - type_: transformation, - source: - DataLocation { - format: source_format, - path: source_path, - }, - destination: - DataLocation { - format: destination_format, - path: destination_path, - }, - } => { - if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; - } - - let source_credential = self.get(&source_format).unwrap(); - - let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); - - let source_value = match finder.find().as_array() { - // todo: still need to investigate other find() return types - Some(array) => array.first().unwrap().clone(), - None => { - return None; - } - }; - - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. - let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); - - let mut leaf_node = construct_leaf_node(&pointer); - - // run the source value through a markdown converter to fit the nested objects into a markdown string - // let image_individualdisplay_source = Value::Array(vec![json!(create_display_parameter(source_value))]); - let image_individualdisplay_source = json!(image_to_elm_media_object(source_value)); - if image_individualdisplay_source.is_null() { - return None; - } - - // let markdown_source_value = json!(image_to_individual_display(source_value)); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(image_individualdisplay_source); - } - - merge(destination_credential, leaf_node); - - trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) - } - - - Transformation::TitleToSpecifiedByObject { - type_: transformation, - source: - DataLocation { - format: source_format, - path: source_path, - }, - destination: - DataLocation { - format: destination_format, - path: destination_path, - }, - } => { - if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; - } - - let source_credential = self.get(&source_format).unwrap(); - - let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); - - let source_value = match finder.find().as_array() { - // todo: still need to investigate other find() return types - Some(array) => array.first().unwrap().clone(), - None => { - return None; - } - }; - - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. - let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); - - let mut leaf_node = construct_leaf_node(&pointer); - // run the source value through a speficfiedby converter to fit the nested objects into a markdown string - let specifiedby_source = json!(title_to_specifiedby(source_value)); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(specifiedby_source); - } - - merge(destination_credential, leaf_node); - - trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) - } - - Transformation::CreditToSpecifiedByObject { - type_: transformation, - source: - DataLocation { - format: source_format, - path: source_path, - }, - destination: - DataLocation { - format: destination_format, - path: destination_path, - }, - } => { - if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; - } - - let source_credential = self.get(&source_format).unwrap(); - - let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); - - let source_value = match finder.find().as_array() { - // todo: still need to investigate other find() return types - Some(array) => array.first().unwrap().clone(), - None => { - return None; - } - }; - - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. - let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); - - let mut leaf_node = construct_leaf_node(&pointer); - // run the source value through a speficfiedby converter to fit the nested objects into a markdown string - let cred_specifiedby_source = json!(credentialpoint_values_to_object(source_value)); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(cred_specifiedby_source); - } - - merge(destination_credential, leaf_node); - - trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) - } - - Transformation::EqfToSpecifiedByQualification { - type_: transformation, - source: - DataLocation { - format: source_format, - path: source_path, - }, - destination: - DataLocation { - format: destination_format, - path: destination_path, - }, - } => { - if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; - } - - let source_credential = self.get(&source_format).unwrap(); - - let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); - - let source_value = match finder.find().as_array() { - // todo: still need to investigate other find() return types - Some(array) => array.first().unwrap().clone(), - None => { - return None; - } - }; - - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. - let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); - - let mut leaf_node = construct_leaf_node(&pointer); - // run the source value through a speficfiedby converter to fit the nested objects into a markdown string - let cred_specifiedby_source = json!(eqf_to_specifiedby_qualification(source_value)); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(cred_specifiedby_source); - } - - merge(destination_credential, leaf_node); - - trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) - } - - Transformation::AddressToLocation { - type_: transformation, - source: - DataLocation { - format: source_format, - path: source_path, - }, - destination: - DataLocation { - format: destination_format, - path: destination_path, - }, - } => { - if source_format != mapping.input_format() || destination_format != mapping.output_format() { - return None; - } - - let source_credential = self.get(&source_format).unwrap(); - - let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); - - let source_value = match finder.find().as_array() { - // todo: still need to investigate other find() return types - Some(array) => array.first().unwrap().clone(), - None => { - return None; - } - }; - - let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. - let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); - - let mut leaf_node = construct_leaf_node(&pointer); - // run the source value through a markdown converter to fit the nested objects into a markdown string - // let image_individualdisplay_source = Value::Array(vec![json!(create_display_parameter(source_value))]); - let location_source = json!(address_to_location(source_value)); - // let markdown_source_value = json!(image_to_individual_display(source_value)); - - if let Some(value) = leaf_node.pointer_mut(&pointer) { - *value = transformation.apply(location_source); - } - - merge(destination_credential, leaf_node); - - trace_dbg!("Successfully completed transformation"); - Some((destination_path, source_path)) - } - - _ => todo!(), - } - } - - pub fn apply_transformations( - &mut self, - transformations: Vec, - mapping: Mapping, - ) -> Vec<(String, String)> { - let mut completed_fields: Vec<(String, String)> = Vec::new(); - - for transformation in transformations { - if let Some(completed_field) = self.apply_transformation(transformation, mapping) { - trace_dbg!(&completed_field); - completed_fields.push(completed_field); - } - } - - completed_fields - } - - pub fn clear_mapping(&mut self, mut output_pointer: String, mapping: Mapping) { - let output_json = self.get_mut(&mapping.output_format()).unwrap(); - - output_pointer = output_pointer.trim_start_matches("/").to_string(); - let keys: Vec = output_pointer.split('/').map(|s| s.to_string()).collect(); - - remove_key_recursive(output_json, &keys); - } -} - -pub fn merge(a: &mut Value, b: Value) { - match (a, b) { - (a @ &mut Value::Object(_), Value::Object(b)) => { - let a = a.as_object_mut().unwrap(); - for (k, v) in b { - merge(a.entry(k).or_insert(Value::Null), v); - } - } - (a @ &mut Value::Array(_), Value::Array(b_arr)) => { - let a_arr = a.as_array_mut().unwrap(); - let a_len = a_arr.len(); - let b_iter = b_arr.into_iter(); - - for (i, b_val) in b_iter.enumerate() { - if i < a_len { - merge(&mut a_arr[i], b_val); - } else { - a_arr.push(b_val); - } - } - } - (_, Value::Null) => { - // If the incoming merge Json Value is `Value::Null`, do nothing to the existing Json Value,the current repository, `a` - } - (a, b) => *a = b, - } -} - -pub fn update_repository(state: &mut AppState) { - let output_pointer = state.output_pointer.clone(); - let output_format = state.mapping.output_format(); - - let source_value = state.candidate_output_value.clone(); - let output_json = state.repository.get_mut(&output_format).unwrap(); - - let mut leaf_node = construct_leaf_node(&output_pointer); - - if let Some(value) = leaf_node.pointer_mut(&output_pointer) { - *value = serde_json::from_str(&source_value).unwrap(); - } - - merge(output_json, leaf_node); -} - -fn remove_key_recursive(current_json: &mut Value, keys: &[String]) -> bool { - if let Some((first, rest)) = keys.split_first() { - if let Some(obj) = current_json.as_object_mut() { - if rest.is_empty() { - obj.remove(first); - } else if let Some(child) = obj.get_mut(first) { - // Recursively traverse deeper layers - if remove_key_recursive(child, rest) { - // If the child is now empty, remove it - obj.remove(first); - } - } - - // Return true if the current object is empty - return obj.is_empty(); - } - } - - false -} - -fn values_to_identity(identity_type: &str, identity_value: Value) -> Value { - //Create a new identity object that is fit for puprose in OBv3 (so not to lose information) - - let mut new_object = Map::new(); - new_object.insert("type".to_string(), Value::String("IdentityObject".to_string())); - new_object.insert("identityHash".to_string(), identity_value); - new_object.insert("identityType".to_string(), Value::String(identity_type.to_string())); - new_object.insert("hashed".to_string(), Value::Bool(false)); - new_object.insert("salt".to_string(), Value::String("not-used".to_string())); - // let _current_value = Value::Object(new_object); - Value::Array(vec![Value::Object(new_object)]) -} - -fn identity_to_object(identity_type: &str, identity_value: Value) -> Value { - //inspect the identity object and re write it so it can be reused in ELM - - //we need to achieve the followin structures: - // "identifier": [ - // { - // "id": "urn:epass:identifier:2", - // "type": "Identifier", - // "notation": "75541452", - // "schemeName": "Student ID" - // } - // ], - - // and for example - // "givenName": { - // "en": ["David"] - // }, - - if let Some(id_value) = identity_value.get("identityHash") { - if identity_type.eq(&"Student ID".to_string()) { - let mut new_object = Map::new(); - new_object.insert("id".to_string(), Value::String("urn:epass:identifier:2".to_string())); - new_object.insert("type".to_string(), Value::String("Identifier".to_string())); - new_object.insert("notation".to_string(), id_value.clone()); - new_object.insert("schemeName".to_string(), Value::String(identity_type.to_string())); - // return the value arrray - Value::Array(vec![Value::Object(new_object)]) - } else { - id_value.clone() - } - } else { - Value::String("".to_string()) - } -} - -fn json_to_markdown(json: &Value, indent_level: usize) -> String { - let mut markdown = String::new(); - let indent = " ".repeat(indent_level); - - match json { - // Handle JSON objects (key-value pairs) - Value::Object(map) => { - for (key, value) in map { - // Add the key as a bold label - markdown.push_str(&format!("{}**{}**:\n", indent, key)); - // Recursively handle the value - markdown.push_str(&json_to_markdown(value, indent_level + 1)); - } - } - - // Handle JSON arrays - Value::Array(array) => { - for item in array { - // Add each array item as a list item - markdown.push_str(&format!("{}- ", indent)); - markdown.push_str(&json_to_markdown(item, indent_level + 1)); - } - } - - // Handle primitive types: strings, numbers, booleans, and null - Value::String(s) => markdown.push_str(&format!("{}{}\n", indent, s)), - Value::Number(n) => markdown.push_str(&format!("{}{}\n", indent, n)), - Value::Bool(b) => markdown.push_str(&format!("{}{}\n", indent, b)), - Value::Null => markdown.push_str(&format!("{}null\n", indent)), - } - - markdown -} - -fn markdown_to_json(lines: &[&str]) -> Value { - // Recursively converts indented lines of Markdown into a JSON structure. - // 1. Parsing Markdown: - // • Headings (#): These are treated as keys in the resulting JSON object. - // • Bold Text (**): This is also treated as a key in the JSON object. - // • List Items (-): These are treated as elements in a JSON array. - // • Plain Text: If it’s not part of a list or a key, it’s treated as a value associated with the last key in the current JSON object. - // 2. Indentation Handling: - // • The code tracks the current indentation level of the Markdown. If the indentation increases, it means a new nested structure (object or array) is starting. If it decreases, the last completed structure is attached to the parent object or array. - // 3. Stack Management: - // • A stack is used to manage the nested structure. Each time a new nested object or array is detected, it’s pushed onto the stack. Once the nesting ends (indentation decreases), the structure is popped from the stack and integrated into the parent structure. - // 4. Regex Patterns: - // • heading_regex: Matches Markdown headings (e.g., # Title). - // • bold_regex: Matches bolded keys (e.g., **Key**:). - // • list_item_regex: Matches list items (e.g., - item). - - let mut i = 0; - let mut position: Vec = Vec::new(); - - //lets create a string to which we will concatenate new lines based on the markdown lines - position.insert(0, "".to_string()); - let mut json_string = String::from(""); - // evaluate if the input contains markdown or not - // if markdown is detected we will try to create json otherwise the value of the "markdown" will be put straight into the attribute - if !lines.contains(&"**") { - while i < lines.len() { - let line = lines[i]; - json_string.push_str(line); - i += 1; - } - let parsed_json: Value = serde_json::to_value(&json_string).unwrap(); - parsed_json - } else { - while i < lines.len() { - let line = lines[i]; - - // Handle key-value pairs (e.g., **key**: value) - let (obj_type, depth) = evaluate_line(line); - if obj_type == "E" && i == 0 { - // open a json object - json_string.push_str("{\n"); - } - - if obj_type == "O" { - while depth < position.len() - 1 { - // we need to close the positions - if let Some(last_value) = position.last() { - if last_value == "O" { - // The last value is O we can now close this object - if let Some(_last_value) = position.pop() { - json_string.pop(); - json_string = json_string.trim_end_matches(',').to_string(); - json_string.push_str("},\n"); - } - } else if last_value == "A" { - // close value A - if let Some(_last_value) = position.pop() { - json_string.push_str("]\n"); - } - } else if last_value == "OA" && depth < position.len() - 1 { - //The last value is OA - if let Some(_last_value) = position.pop() { - // first close the array - json_string.pop(); - json_string = json_string.trim_end_matches(',').to_string(); - json_string.push_str("],\n"); - if depth > 0 { - if let Some(_last_value) = position.pop() { - // then close the object - json_string.push_str("},\n"); - } - } else { - //"The vector was empty, nothing to remove."); - } - } else { - // ("The vector was empty, nothing to remove."); - } - } else { - // we have a different last value we will remove it from tha vector - if let Some(_last_value) = position.pop() { - // json_string.push_str("]\n"); - } - } - } else { - // println!("The vector is empty."); - } - } - - // setup the vector array for the right value and position - if depth >= position.len() - 1 && i > 0 { - //test previous line to see is we might have a nested object - let (last_obj_type, _new_depth) = evaluate_line(lines[i - 1]); - if last_obj_type == "O" { - json_string.push('{'); - json_string.push_str(&cleanup_string(line)); - json_string.push(':'); - position.insert(depth, "O".to_string()); - } else if let Some(last_value) = position.last() { - if last_value == "OA" && depth == position.len() - 1 { - json_string.push_str(&cleanup_string(line)); - json_string.push(':'); - } else if depth > position.len() - 1 { - json_string.push('{'); - json_string.push_str(&cleanup_string(line)); - json_string.push(':'); - position.insert(depth, "O".to_string()); - } else { - json_string.push_str(&cleanup_string(line)); - json_string.push(':'); - position[depth] = "O".to_string(); - } - } - } - } else if obj_type == "A" { - while depth < position.len() - 1 { - // we need to close the positions - if let Some(last_value) = position.last() { - if last_value == "O" { - if let Some(_last_value) = position.pop() { - json_string.push_str("},\n"); - } - } else if last_value == "A" { - if let Some(_last_value) = position.pop() { - json_string.push_str("]\n"); - } - } else { - // The last value is something leave it - } - } else { - // The vector is empty. - } - } - - // setup the vector array for the right value and position - if depth >= position.len() - 1 { - if let Some(last_value) = position.last() { - if last_value == "OA" { - json_string.push('['); - } else if last_value == "A" && depth == position.len() - 1 { - position.insert(depth, "A".to_string()); - } else { - position.insert(depth, "A".to_string()); - json_string.push('['); - } - } - json_string.push_str(&cleanup_string(line)); - } - } else if obj_type == "OA" { - while depth < position.len() - 1 { - // we need to close the positions - if let Some(last_value) = position.last() { - if last_value == "O" { - if let Some(_last_value) = position.pop() { - json_string.pop(); - json_string.pop(); - json_string.push_str("},\n"); - } - } else if last_value == "A" { - if let Some(_last_value) = position.pop() { - json_string.push_str("]\n"); - } else { - // The vector was empty, nothing to remove. - } - } else { - // The last value is something else leave it - } - } else { - // The vector is empty - } - } - - // we are creating a new array that will contain objects of the same type - json_string.push_str("[ \n {"); - json_string.push_str(&cleanup_string(line)); - json_string.push(':'); - // test if extra handling is needed for closing - position.insert(depth - 1, "OA".to_string()); - position.insert(depth, "O".to_string()); - } else if obj_type == "V" { - json_string.push_str(&cleanup_string(line)); - json_string.push_str(",\n"); - } - - i += 1; - } - - // Finalize the string to which we will concatenate new lines based on the markdown lines - json_string.pop(); - json_string = json_string.trim_end_matches(',').to_string(); - json_string.push_str("\n}"); - let parsed_json: Value = serde_json::from_str(&json_string).unwrap(); - parsed_json - } -} - -fn evaluate_line(line_to_test: &str) -> (String, usize) { - //test depth - let mut depth = line_to_test.chars().take_while(|c| c.is_whitespace()).count() / 2; - //test type - let line_type; - let trimmed = line_to_test.trim(); - if line_to_test.is_empty() { - // Handle list as object items of previous depth - line_type = "E"; - } else if trimmed.starts_with("-") && trimmed.ends_with("**:") { - // Handle list as object items of previous depth - line_type = "OA"; - depth += 1; - } else if trimmed.starts_with("**") { - // Handle list as object items of previous depth - line_type = "O"; - } else if trimmed.starts_with("-") { - // Handle as array items of previous depth - line_type = "A"; - } else { - // Handle value of previous depth - line_type = "V"; - } - - (line_type.to_string(), depth) -} - -fn cleanup_string(string_to_clean: &str) -> String { - //trim the string - let string_to_clean1 = string_to_clean.trim(); - let string_to_clean2 = string_to_clean1.replace("-", ""); - let string_to_clean3 = string_to_clean2.trim(); - let string_to_clean4 = string_to_clean3.replace("**:", ""); - let cleaned_string = string_to_clean4.trim().trim_matches('*').to_string(); - // Add quotes around the cleaned string - format!("\"{}\"", cleaned_string) -} diff --git a/src/backend/repository.rs b/src/backend/repository.rs index d5167d7..bf545c0 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -2,8 +2,9 @@ use crate::{ backend::{ base64_encode::{create_display_parameter, image_to_elm_media_object}, elm_mapping_helper::{ - address_to_location, credentialpoint_values_to_object, eqf_to_specifiedby_qualification, - title_to_specifiedby, + address_to_location, assessment_type_to_specifiedby_assesment, create_learning_outcome_summary, + credentialpoint_values_to_object, eqf_to_specifiedby_qualification, object_to_note_literal, + title_to_specifiedby, transform_learning_outcomes, }, jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, @@ -628,6 +629,184 @@ impl Repository { Ok(Some((destination_path, source_path))) } + Transformation::AssessmentToProvenBy { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return Ok(None); + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return Ok(None); + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + // run the source value through a speficfiedby converter to fit the nested objects into a markdown string + let assess_specifiedby_source = json!(assessment_type_to_specifiedby_assesment(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(assess_specifiedby_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Ok(Some((destination_path, source_path))) + } + + Transformation::ObjectToNoteLiteral { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return Ok(None); + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return Ok(None); + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + let mut leaf_node = construct_leaf_node(&pointer); + // run the source value through a speficfiedby converter to fit the nested objects into a markdown string + let object_source = json!(object_to_note_literal(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(object_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Ok(Some((destination_path, source_path))) + } + + Transformation::TranslateLearningOutcome { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return Ok(None); + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return Ok(None); + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + let mut leaf_node = construct_leaf_node(&pointer); + // run the source value through a speficfiedby converter to fit the nested objects into a markdown string + let learning_outcome_source = json!(transform_learning_outcomes(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(learning_outcome_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Ok(Some((destination_path, source_path))) + } + + Transformation::CreateLearningOutcomeSummary { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return Ok(None); + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return Ok(None); + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + let mut leaf_node = construct_leaf_node(&pointer); + // run the source value through a speficfiedby converter to fit the nested objects into a markdown string + let learning_outcome_sum_source = json!(create_learning_outcome_summary(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(learning_outcome_sum_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Ok(Some((destination_path, source_path))) + } + Transformation::AddressToLocation { type_: transformation, source: diff --git a/src/backend/routes/api.rs b/src/backend/routes/api.rs index a0fccdb..0475b67 100644 --- a/src/backend/routes/api.rs +++ b/src/backend/routes/api.rs @@ -24,9 +24,9 @@ pub async fn api(Json(input_json): Json) -> impl IntoResponse { // } // Handle the file upload - let mut _input_file_path = String::new(); - let mut _mapping_file_name = String::new(); - let mut _mapping_type = Mapping::default(); + let input_file_path; + let mapping_file_name; + let mapping_type; // Create directories for uploads and outputs if they don't exist let upload_dir = "uploads"; @@ -51,12 +51,12 @@ pub async fn api(Json(input_json): Json) -> impl IntoResponse { .and_then(|v| v.as_str()) { Some("OB") => { - _mapping_file_name = "json/mapping/custom_mapping_OBv3_ELM_latest.json".to_string(); - _mapping_type = Mapping::OBv3ToELM; + mapping_file_name = "json/mapping/custom_mapping_OBv3_ELM_latest.json".to_string(); + mapping_type = Mapping::OBv3ToELM; } Some("ELM") => { - _mapping_file_name = "json/mapping/custom_mapping_ELM_OBv3_latest.json".to_string(); - _mapping_type = Mapping::ELMToOBv3; + mapping_file_name = "json/mapping/custom_mapping_ELM_OBv3_latest.json".to_string(); + mapping_type = Mapping::ELMToOBv3; } Some(value) => { let error_json = json!({ @@ -74,8 +74,8 @@ pub async fn api(Json(input_json): Json) -> impl IntoResponse { match input_json.get("Content").and_then(|v| v.as_str()) { Some(value) => { - _input_file_path = format!("{}/{}", upload_dir, file_name); - let file = File::create(&_input_file_path).map_err(|_| { + input_file_path = format!("{}/{}", upload_dir, file_name); + let file = File::create(&input_file_path).map_err(|_| { let error_json = json!({ "error": "Internal Server Error", "message" : "Failed to create file"}); @@ -115,7 +115,7 @@ pub async fn api(Json(input_json): Json) -> impl IntoResponse { // Define the output file path let output_file_name = format!( "translated_{}", - Path::new(&_input_file_path).file_name().unwrap().to_str().unwrap() + Path::new(&input_file_path).file_name().unwrap().to_str().unwrap() ); let output_file_path = format!("{}/{}", output_dir, output_file_name); @@ -123,11 +123,18 @@ pub async fn api(Json(input_json): Json) -> impl IntoResponse { // 1 create a state needed for the mapping tool // 2 load all hte state elements needed for mapping - let mut state = AppState::default(); - state.input_path = _input_file_path; - state.output_path = output_file_path.clone(); - state.mapping_path = _mapping_file_name; - state.mapping = _mapping_type; + let mut state = AppState { + input_path: input_file_path, + output_path: output_file_path.clone(), + mapping_path: mapping_file_name, + mapping: mapping_type, + ..Default::default() + }; + // let mut state = AppState::default(); + // state.input_path = _input_file_path; + // state.output_path = output_file_path.clone(); + // state.mapping_path = _mapping_file_name; + // state.mapping = _mapping_type; load_files_apply_transformations(&mut state); @@ -187,9 +194,7 @@ pub async fn api(Json(input_json): Json) -> impl IntoResponse { (StatusCode::INTERNAL_SERVER_ERROR, Json(error_json)) } }, - Err(status) => { - status - } + Err(status) => status, } // let encoded_json = encode_json_file(output_file)?; diff --git a/src/backend/routes/translate_file.rs b/src/backend/routes/translate_file.rs index 1ec91f7..c3aff61 100644 --- a/src/backend/routes/translate_file.rs +++ b/src/backend/routes/translate_file.rs @@ -98,11 +98,18 @@ pub async fn translate_file(mut multipart: Multipart) -> Result Value { + match self { + AssessmentToProvenBy::assessmentToProvenBy => value, + } + } +} + +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum ObjectToNoteLiteral { + objectToNoteLiteral, +} + +impl ObjectToNoteLiteral { + pub fn apply(&self, value: Value) -> Value { + match self { + ObjectToNoteLiteral::objectToNoteLiteral => value, + } + } +} + +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum TranslateLearningOutcome { + translateLearningOutcome, +} + +impl TranslateLearningOutcome { + pub fn apply(&self, value: Value) -> Value { + match self { + TranslateLearningOutcome::translateLearningOutcome => value, + } + } +} + +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum CreateLearningOutcomeSummary { + createLearningOutcomeSummary, +} + +impl CreateLearningOutcomeSummary { + pub fn apply(&self, value: Value) -> Value { + match self { + CreateLearningOutcomeSummary::createLearningOutcomeSummary => value, + } + } +} + #[allow(non_camel_case_types)] #[derive(Serialize, Deserialize, Debug, Clone)] pub enum CreditToSpecifiedByObject { @@ -326,6 +382,26 @@ pub enum Transformation { source: DataLocation, destination: DataLocation, }, + AssessmentToProvenBy { + type_: AssessmentToProvenBy, + source: DataLocation, + destination: DataLocation, + }, + ObjectToNoteLiteral { + type_: ObjectToNoteLiteral, + source: DataLocation, + destination: DataLocation, + }, + TranslateLearningOutcome { + type_: TranslateLearningOutcome, + source: DataLocation, + destination: DataLocation, + }, + CreateLearningOutcomeSummary { + type_: CreateLearningOutcomeSummary, + source: DataLocation, + destination: DataLocation, + }, AddressToLocation { type_: AddressToLocation, source: DataLocation, diff --git a/test/uvh_at_microcredential_full_1.json b/test/uvh_at_microcredential_full_1.json index 3629f4e..d7639b4 100644 --- a/test/uvh_at_microcredential_full_1.json +++ b/test/uvh_at_microcredential_full_1.json @@ -107,6 +107,26 @@ "targetUrl": "https://content.example.com/description-eqf-levels" } ], + "learningOutcome": [ + { + "id": "urn:epass:learningOutcome:1", + "type": "LearningOutcome", + "relatedSkill": { + "id": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", + "title": "5.4 Identifying digital competence gaps" + }, + "title": "Name of DigiComp Competence" + }, + { + "id": "urn:epass:learningOutcome:3", + "type": "LearningOutcome", + "relatedSkill": { + "id": "http://data.europa.eu/snb/dcf/34v10n662m", + "title": "3.1 Proficiency Level Foundation 2" + }, + "title": "Name of DigiComp Competence 2" + } + ], "participationType": "onsite or blended", "assessmentType": "testing", "identityChecked": true, From bcd0fbd389fc5283ca1056589815085433bb7c31 Mon Sep 17 00:00:00 2001 From: hamrt Date: Mon, 10 Feb 2025 01:09:33 +0100 Subject: [PATCH 42/45] add description to achievement --- .../custom_mapping_OBv3_ELM_latest.json | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index 5c71cb6..5b13271 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -85,6 +85,9 @@ "path": "$.issuer.logo" } }, + + + { "type_": "copy", "source": { @@ -118,6 +121,8 @@ "path": "$.issued" } }, + + { "type_": "copy", "source": { @@ -163,6 +168,9 @@ "path": "$.credentialSubject.fullName.en[0]" } }, + + + { "type_": "imageToIndividualDisplay", "source": { @@ -259,6 +267,18 @@ "path": "$.credentialSubject.hasClaim[0].title.en[0]" } }, + { + "type_": "copy", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.description" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].description.en[0]" + } + }, + { "type_": "copy", "source": { @@ -325,7 +345,17 @@ "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcome" } }, - + { + "type_": "createLearningOutcomeSummary", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.criteria.narrative" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcomeSummary" + } + }, { @@ -371,16 +401,5 @@ "format": "ELM", "path": "$.credentialSubject.hasClaim[0].provenBy[0].grade.noteLiteral.en[0]" } - }, - { - "type_": "createLearningOutcomeSummary", - "source": { - "format": "OBv3", - "path": "$.credentialSubject.achievement.criteria.narrative" - }, - "destination": { - "format": "ELM", - "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningOutcomeSummary" - } } ] \ No newline at end of file From a6404934e9c05ae6041c23e5289d3910fad5156c Mon Sep 17 00:00:00 2001 From: hamrt Date: Fri, 21 Feb 2025 09:42:43 +0100 Subject: [PATCH 43/45] add learningsetting update --- .../custom_mapping_OBv3_ELM_latest.json | 11 ++++ .../Complete_OpenBadgeCredential_image.json | 60 ------------------- src/backend/elm_mapping_helper.rs | 60 ++++++++++++++++++- src/backend/repository.rs | 50 +++++++++++++++- src/backend/transformations.rs | 19 ++++++ 5 files changed, 135 insertions(+), 65 deletions(-) diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index 5b13271..6fe886a 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -401,5 +401,16 @@ "format": "ELM", "path": "$.credentialSubject.hasClaim[0].provenBy[0].grade.noteLiteral.en[0]" } + }, + { + "type_": "learningSettingToSpecifiedByObject", + "source": { + "format": "OBv3", + "path": "$.credentialSubject.achievement.learningSetting" + }, + "destination": { + "format": "ELM", + "path": "$.credentialSubject.hasClaim[0].specifiedBy.learningSetting" + } } ] \ No newline at end of file diff --git a/json/obv3/examples/Complete_OpenBadgeCredential_image.json b/json/obv3/examples/Complete_OpenBadgeCredential_image.json index d77238d..82a627e 100644 --- a/json/obv3/examples/Complete_OpenBadgeCredential_image.json +++ b/json/obv3/examples/Complete_OpenBadgeCredential_image.json @@ -327,58 +327,11 @@ } ], "resultDescription": [ - { - "id": "urn:uuid:f6ab24cd-86e8-4eaf-b8c6-ded74e8fd41c", - "type": [ - "ResultDescription" - ], - "alignment": [ - { - "type": [ - "Alignment" - ], - "targetCode": "project", - "targetDescription": "Project description", - "targetName": "Final Project", - "targetFramework": "1EdTech University Program and Course Catalog", - "targetType": "CFItem", - "targetUrl": "https://1edtech.edu/catalog/degree/project" - } - ], - "allowedValue": [ - "D", - "C", - "B", - "A" - ], - "name": "Final Project Grade", - "requiredValue": "C", - "resultType": "LetterGrade" - }, { "id": "urn:uuid:a70ddc6a-4c4a-4bd8-8277-cb97c79f40c5", "type": [ "ResultDescription" ], - "alignment": [ - { - "type": [ - "Alignment" - ], - "targetCode": "project", - "targetDescription": "Project description", - "targetName": "Final Project", - "targetFramework": "1EdTech University Program and Course Catalog", - "targetType": "CFItem", - "targetUrl": "https://1edtech.edu/catalog/degree/project" - } - ], - "allowedValue": [ - "D", - "C", - "B", - "A" - ], "name": "Final Project Grade", "requiredLevel": "urn:uuid:d05a0867-d0ad-4b03-bdb5-28fb5d2aab7a", "resultType": "RubricCriterionLevel", @@ -388,19 +341,6 @@ "type": [ "RubricCriterionLevel" ], - "alignment": [ - { - "type": [ - "Alignment" - ], - "targetCode": "project", - "targetDescription": "Project description", - "targetName": "Final Project", - "targetFramework": "1EdTech University Program and Course Catalog", - "targetType": "CFRubricCriterionLevel", - "targetUrl": "https://1edtech.edu/catalog/degree/project/rubric/levels/mastered" - } - ], "description": "The author demonstrated...", "level": "Mastered", "name": "Mastery", diff --git a/src/backend/elm_mapping_helper.rs b/src/backend/elm_mapping_helper.rs index cd3447a..db04784 100644 --- a/src/backend/elm_mapping_helper.rs +++ b/src/backend/elm_mapping_helper.rs @@ -81,7 +81,7 @@ pub fn title_to_specifiedby(title: Value) -> Value { "id": "urn:epass:learningAchievementSpec:1", "type": "Qualification", "title": { - "en": ["Data and soferetware business"] + "en": ["Data and software business"] } } "#; @@ -152,7 +152,7 @@ pub fn credentialpoint_values_to_object(credits: Value) -> Value { parsed_json } -/// Creates specifiedBy based on input type in string found title +/// Creates EQF values in specifiedBy based on input type in string found title /// /// # Arguments /// - `alignment`: an array that could be found in OBv3 but needs to be translated to fit the new structure of ELM. @@ -349,6 +349,62 @@ pub fn create_learning_outcome_summary(json_obj: Value) -> Value { } } +/// Creates learningSetting based on string provided by OBv3 string found in custom value learningSetting +/// Should land in specifiedBy +/// +/// # Arguments +/// - `alignment`: an array that could be found in OBv3 but needs to be translated to fit the new structure of ELM. +/// +/// # Returns +/// - Value: The content value Object in ELM format if successful. +pub fn transform_learning_setting(learning_setting: Value) -> Value { + //inspect the title object and re write it so it can be reused in ELM for building a creditpoint that cn be used in Specification + //we need to achieve the following structure for a creditpoint: + let json_data = r#" + { + "id": "http://data.europa.eu/snb/learning-setting/e207a81fc7", + "type": "Concept", + "inScheme": { + "id": "http://data.europa.eu/snb/learning-setting/25831c2", + "type": "ConceptScheme" + }, + "prefLabel": { + "en": ["non-formal"] + } + } + "#; + + let mut parsed_json: Value = serde_json::from_str(json_data).unwrap(); + //println!("{:#?}", alignment); + // Extract the array from the Value + if let Some(learning_setting_str) = learning_setting.as_str() { + + match learning_setting_str { + "formal learning" | "formal" => { + parsed_json["id"] = Value::String("http://data.europa.eu/snb/learning-setting/6fd4685715".to_string()); + parsed_json["inScheme"]["id"]= Value::String("http://data.europa.eu/snb/learning-setting/25831c2".to_string()); + parsed_json["prefLabel"]["en"][0] = Value::String("formal learning".to_string()); + } + "non-formal" | "nonformal" => { + parsed_json["id"] = Value::String("http://data.europa.eu/snb/learning-setting/6fd4685715".to_string()); + parsed_json["inScheme"]["id"]= Value::String("http://data.europa.eu/snb/learning-setting/25831c2".to_string()); + parsed_json["prefLabel"]["en"][0] = Value::String("non-formal".to_string()); + } + _ => { return Value::Null; } + } + } else { + //println!("Error: Data is not an array."); + return Value::Null; + } + + //println!("{:#?}", parsed_json); + parsed_json +} + + + + + // additional private helpers // Function to handle both single object and array of objects fn handle_json_input(json_obj: &Value) -> String { diff --git a/src/backend/repository.rs b/src/backend/repository.rs index bf545c0..6f71011 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -2,9 +2,7 @@ use crate::{ backend::{ base64_encode::{create_display_parameter, image_to_elm_media_object}, elm_mapping_helper::{ - address_to_location, assessment_type_to_specifiedby_assesment, create_learning_outcome_summary, - credentialpoint_values_to_object, eqf_to_specifiedby_qualification, object_to_note_literal, - title_to_specifiedby, transform_learning_outcomes, + address_to_location, assessment_type_to_specifiedby_assesment, create_learning_outcome_summary, credentialpoint_values_to_object, eqf_to_specifiedby_qualification, object_to_note_literal, title_to_specifiedby, transform_learning_setting, transform_learning_outcomes }, jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, @@ -629,6 +627,52 @@ impl Repository { Ok(Some((destination_path, source_path))) } + Transformation::LearningSettingToSpecifiedByObject { + type_: transformation, + source: + DataLocation { + format: source_format, + path: source_path, + }, + destination: + DataLocation { + format: destination_format, + path: destination_path, + }, + } => { + if source_format != mapping.input_format() || destination_format != mapping.output_format() { + return Ok(None); + } + + let source_credential = self.get(&source_format).unwrap(); + + let finder = JsonPathFinder::from_str(&source_credential.to_string(), &source_path).unwrap(); + + let source_value = match finder.find().as_array() { + // todo: still need to investigate other find() return types + Some(array) => array.first().unwrap().clone(), + None => { + return Ok(None); + } + }; + + let destination_credential = self.entry(destination_format).or_insert(json!({})); // or_insert should never happen, since repository is initialized with all formats, incl empty json value when not present. + let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); + + let mut leaf_node = construct_leaf_node(&pointer); + // run the source value through a speficfiedby converter to fit the nested objects into a markdown string + let learning_setting_specifiedby_source = json!(transform_learning_setting(source_value)); + + if let Some(value) = leaf_node.pointer_mut(&pointer) { + *value = transformation.apply(learning_setting_specifiedby_source); + } + + merge(destination_credential, leaf_node); + + trace_dbg!("Successfully completed transformation"); + Ok(Some((destination_path, source_path))) + } + Transformation::AssessmentToProvenBy { type_: transformation, source: diff --git a/src/backend/transformations.rs b/src/backend/transformations.rs index da8e5d5..bdf2efe 100644 --- a/src/backend/transformations.rs +++ b/src/backend/transformations.rs @@ -277,6 +277,20 @@ impl EqfToSpecifiedByQualification { } } +#[allow(non_camel_case_types)] +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum LearningSettingToSpecifiedByObject { + learningSettingToSpecifiedByObject, +} + +impl LearningSettingToSpecifiedByObject { + pub fn apply(&self, value: Value) -> Value { + match self { + LearningSettingToSpecifiedByObject::learningSettingToSpecifiedByObject => value, + } + } +} + #[allow(non_camel_case_types)] #[derive(Serialize, Deserialize, Debug, Clone)] pub enum TitleToSpecifiedByObject { @@ -377,6 +391,11 @@ pub enum Transformation { source: DataLocation, destination: DataLocation, }, + LearningSettingToSpecifiedByObject { + type_: LearningSettingToSpecifiedByObject, + source: DataLocation, + destination: DataLocation, + }, CreditToSpecifiedByObject { type_: CreditToSpecifiedByObject, source: DataLocation, From 72916ca703f20179ca5f8eab9345b744c07879e8 Mon Sep 17 00:00:00 2001 From: hamrt Date: Mon, 24 Feb 2025 17:00:20 +0100 Subject: [PATCH 44/45] update for learning outcome handling (based on 1EdTech proposal) --- .../custom_mapping_OBv3_ELM_latest.json | 2 +- src/backend/elm_mapping_helper.rs | 134 +++++++---- src/backend/repository.rs | 7 +- ...crocredential_full_edtech_proposal_LO.json | 216 ++++++++++++++++++ 4 files changed, 310 insertions(+), 49 deletions(-) create mode 100644 test/uvh_at_microcredential_full_edtech_proposal_LO.json diff --git a/json/mapping/custom_mapping_OBv3_ELM_latest.json b/json/mapping/custom_mapping_OBv3_ELM_latest.json index 6fe886a..f8e50b8 100644 --- a/json/mapping/custom_mapping_OBv3_ELM_latest.json +++ b/json/mapping/custom_mapping_OBv3_ELM_latest.json @@ -338,7 +338,7 @@ "type_": "translateLearningOutcome", "source": { "format": "OBv3", - "path": "$.credentialSubject.achievement.learningOutcome" + "path": "$.credentialSubject.achievement.alignment" }, "destination": { "format": "ELM", diff --git a/src/backend/elm_mapping_helper.rs b/src/backend/elm_mapping_helper.rs index db04784..7c90c01 100644 --- a/src/backend/elm_mapping_helper.rs +++ b/src/backend/elm_mapping_helper.rs @@ -1,5 +1,5 @@ use codes_iso_3166::part_1::CountryCode; -use serde_json::{json, Value}; +use serde_json::{json, Map, Value}; use std::str::FromStr; /// Creates country code based on input type in string found in addressCountryCode @@ -279,6 +279,7 @@ pub fn object_to_note_literal(any_object: Value) -> Value { Value::String(str_array) } + /// Creates learningOutcomes based on input type in outcome array /// /// # Arguments @@ -286,38 +287,82 @@ pub fn object_to_note_literal(any_object: Value) -> Value { /// /// # Returns /// - Value: The specifiedBy object with dummies in ELM format if successful. -pub fn transform_learning_outcomes(json_obj: Value) -> Value { - if let Some(learning_outcomes) = json_obj.as_array() { - let transformed_outcomes: Vec = learning_outcomes - .iter() - .map(|outcome| { - let new_related_skill = outcome.get("relatedSkill").map(|related| { - json!({ - "id": related.get("id").unwrap_or(&json!("")), - "type": "Concept", - "inScheme": { - "id": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2", - "type": "ConceptScheme" - }, - "prefLabel": { - "en": [related.get("title").unwrap_or(&json!("")).as_str().unwrap_or("")] +pub fn transform_alignment_to_learning_outcomes(json_obj: Value) -> Value { + if let Some(alignments) = json_obj.as_array() { + println!("{:#?}", alignments); + let mut results = Vec::new(); + + for alignment in alignments { + // Check if "type" array contains "LearningOutcome" + if let Some(types) = alignment.get("type").and_then(Value::as_array) { + if types.iter().any(|t| t == "LearningOutcome") { + let title = alignment + .get("targetName") + .and_then(Value::as_str) + .unwrap_or("Unknown Title"); + let description = alignment + .get("targetDescription") + .and_then(Value::as_str) + .unwrap_or("") + .to_string(); + let target_url = alignment.get("targetUrl").and_then(Value::as_str).unwrap_or(""); + // Separate ESCO relations from other relations + let mut esco_relations = Vec::new(); + let mut other_relations = Vec::new(); + + if let Some(relations) = alignment.get("relations").and_then(Value::as_array) { + for relation in relations { + if let Some(framework) = relation.get("targetFramework").and_then(Value::as_str) + { + let relation_object = json!({ + "id": relation.get("targetUrl").and_then(Value::as_str), + "type": "Concept", + "inScheme": { + "id": relation.get("frameworkUrl").and_then(Value::as_str), + "type": "ConceptScheme" + }, + "prefLabel": + { + "en": [relation.get("targetName").and_then(Value::as_str)] + }, + "notation": "Skill" + }); + + if framework == "ESCO" { + esco_relations.push(relation_object); + } else { + other_relations.push(relation_object); + } + } + } + + // **Use Map to construct the object dynamically** + let mut learning_outcome = Map::new(); + + learning_outcome.insert("title".to_string(), json!({"en": [title]})); + learning_outcome.insert("type".to_string(), Value::String("LearningOutcome".to_string())); + if !description.is_empty() { + learning_outcome.insert("additionalNote".to_string(), json!([{"id": "urn:epass:note:3", "type": "Note", "noteLiteral": {"en": [description]}}])); } - }) - }); + learning_outcome.insert("id".to_string(), Value::String(target_url.to_string())); + + // **Only add arrays if they are NOT empty** + if !esco_relations.is_empty() { + learning_outcome.insert("relatedESCOSkill".to_string(), Value::Array(esco_relations)); + } + if !other_relations.is_empty() { + learning_outcome.insert("relatedSkill".to_string(), Value::Array(other_relations)); + } + results.push(Value::Object(learning_outcome)); - let mut new_outcome = outcome.clone(); - if let Some(obj) = new_outcome.as_object_mut() { - if let Some(new_related_skill) = new_related_skill { - obj.insert("relatedSkill".to_string(), new_related_skill); } - obj.insert("title".to_string(), json!({"en": [outcome.get("title")]})); } - // Modify fields inside the object - new_outcome - }) - .collect(); - Value::Array(transformed_outcomes) + // println!("{:#?}", results); + } + } + + Value::Array(results) } else { Value::Null } @@ -378,20 +423,23 @@ pub fn transform_learning_setting(learning_setting: Value) -> Value { //println!("{:#?}", alignment); // Extract the array from the Value if let Some(learning_setting_str) = learning_setting.as_str() { - - match learning_setting_str { - "formal learning" | "formal" => { - parsed_json["id"] = Value::String("http://data.europa.eu/snb/learning-setting/6fd4685715".to_string()); - parsed_json["inScheme"]["id"]= Value::String("http://data.europa.eu/snb/learning-setting/25831c2".to_string()); - parsed_json["prefLabel"]["en"][0] = Value::String("formal learning".to_string()); - } - "non-formal" | "nonformal" => { - parsed_json["id"] = Value::String("http://data.europa.eu/snb/learning-setting/6fd4685715".to_string()); - parsed_json["inScheme"]["id"]= Value::String("http://data.europa.eu/snb/learning-setting/25831c2".to_string()); - parsed_json["prefLabel"]["en"][0] = Value::String("non-formal".to_string()); + match learning_setting_str { + "formal learning" | "formal" => { + parsed_json["id"] = Value::String("http://data.europa.eu/snb/learning-setting/6fd4685715".to_string()); + parsed_json["inScheme"]["id"] = + Value::String("http://data.europa.eu/snb/learning-setting/25831c2".to_string()); + parsed_json["prefLabel"]["en"][0] = Value::String("formal learning".to_string()); + } + "non-formal" | "nonformal" => { + parsed_json["id"] = Value::String("http://data.europa.eu/snb/learning-setting/6fd4685715".to_string()); + parsed_json["inScheme"]["id"] = + Value::String("http://data.europa.eu/snb/learning-setting/25831c2".to_string()); + parsed_json["prefLabel"]["en"][0] = Value::String("non-formal".to_string()); + } + _ => { + return Value::Null; + } } - _ => { return Value::Null; } - } } else { //println!("Error: Data is not an array."); return Value::Null; @@ -401,10 +449,6 @@ pub fn transform_learning_setting(learning_setting: Value) -> Value { parsed_json } - - - - // additional private helpers // Function to handle both single object and array of objects fn handle_json_input(json_obj: &Value) -> String { diff --git a/src/backend/repository.rs b/src/backend/repository.rs index 6f71011..9989f75 100644 --- a/src/backend/repository.rs +++ b/src/backend/repository.rs @@ -2,7 +2,7 @@ use crate::{ backend::{ base64_encode::{create_display_parameter, image_to_elm_media_object}, elm_mapping_helper::{ - address_to_location, assessment_type_to_specifiedby_assesment, create_learning_outcome_summary, credentialpoint_values_to_object, eqf_to_specifiedby_qualification, object_to_note_literal, title_to_specifiedby, transform_learning_setting, transform_learning_outcomes + address_to_location, assessment_type_to_specifiedby_assesment, create_learning_outcome_summary, credentialpoint_values_to_object, eqf_to_specifiedby_qualification, object_to_note_literal, title_to_specifiedby, transform_learning_setting, transform_alignment_to_learning_outcomes }, jsonpointer::{JsonPath, JsonPointer}, leaf_nodes::construct_leaf_node, @@ -795,8 +795,9 @@ impl Repository { let pointer = JsonPointer::try_from(JsonPath(destination_path.clone())).unwrap(); let mut leaf_node = construct_leaf_node(&pointer); // run the source value through a speficfiedby converter to fit the nested objects into a markdown string - let learning_outcome_source = json!(transform_learning_outcomes(source_value)); - + // let learning_outcome_source = json!(transform_learning_outcomes(source_value)); + let learning_outcome_source = json!(transform_alignment_to_learning_outcomes(source_value)); + if let Some(value) = leaf_node.pointer_mut(&pointer) { *value = transformation.apply(learning_outcome_source); } diff --git a/test/uvh_at_microcredential_full_edtech_proposal_LO.json b/test/uvh_at_microcredential_full_edtech_proposal_LO.json new file mode 100644 index 0000000..a0e0463 --- /dev/null +++ b/test/uvh_at_microcredential_full_edtech_proposal_LO.json @@ -0,0 +1,216 @@ +{ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json", + "https://raw.githubusercontent.com/educredentials/obv3-examples/refs/heads/main/contexts/educredential.json" + ], + "id": "http://example.com/credentials/crd-A1B2C3", + "type": [ + "VerifiableCredential", + "OpenBadgeCredential" + ], + "issuer": { + "id": "https://example.com/issuers/iss-9Z8Y7X", + "type": [ + "Profile" + ], + "name": "Universiteit van Harderwijk", + "otherIdentifier": [ + { + "type": "IdentifierEntry", + "identifier": "UN1VH", + "identifierType": "ext:BRIN" + }, + { + "type": "IdentifierEntry", + "identifier": "uvh.example.com", + "identifierType": "name" + } + ], + "image": { + "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/university_of_harderwijk_75x100.jpg", + "type": "Image", + "caption": "University of Harderwijk logo" + }, + "address": { + "type": [ + "Address" + ], + "addressCountry": "NDL", + "addressCountryCode": "NL", + "addressRegion": "UT", + "addressLocality": "Harderwijk", + "streetAddress": "123 First St", + "postOfficeBoxNumber": "1", + "postalCode": "12345", + "geo": { + "type": "GeoCoordinates", + "latitude": 1, + "longitude": 1 + } + } + }, + "validFrom": "2024-08-30T00:00:00Z", + "validUntil": "2029-08-30T00:00:00Z", + "credentialSubject": { + "id": "https://example.com/students/stu-7Z6X5W", + "type": [ + "AchievementSubject" + ], + "identifier": [ + { + "type": "IdentityObject", + "identityHash": "student@1edtech.edu", + "identityType": "emailAddress", + "hashed": false, + "salt": "not-used" + } + ], + "achievement": { + "id": "https://example.com/achievements/ach-876543", + "type": [ + "Achievement", + "EducredentialAchievement" + ], + "criteria": { + "narrative": "De student heeft de eerste stap van transmutatie afgerond, waarbij katten zonder blijvende schade worden omgezet in keukenapparatuur." + }, + "description": "De student heeft met succes de eerste stap in transmutatie afgerond, waarbij katten zonder blijvende schade worden omgezet in keukenapparatuur.", + "name": "Microcredential Basisomzetting Kat naar Koffiezetapparaat", + "image": { + "id": "https://raw.githubusercontent.com/hamrt/credential-converter/refs/heads/image/test/edubadges_100x100.png", + "type": "Image" + }, + "inLanguage": "nl-NL", + "educationProgramIdentifier": 20121350, + "ECTS": 3.0, + "creditsAvailable": 3, + + "alignment": [ + { + "type": [ + "Alignment" + ], + "targetType": "ext:QualityAssurance", + "targetName": "W Practische toepassing van animale transmutatie", + "targetDescription": "Accreditatie van de opleiding door de Wijze Raad van de MagiĆ«rs", + "targetCode": "AV-7381", + "targetUrl": "https://data.example.com/decisions/AV-7381" + }, + { + "type": [ + "Alignment" + ], + "targetType": "ext:EQF", + "targetName": "EQF level 4", + "targetCode": "4", + "targetUrl": "https://content.example.com/description-eqf-levels" + }, + { + "type": [ + "Alignment", + "LearningOutcome" + ], + "targetName": "Monetizing with data and software", + "targetUrl": "http://myinstitute.eu/LearningOutcome-1", + "targetCode": "LO1", + "targetDescription": "- Student understands the basic principles of data and software business, and the special characteristics of software industry\n- He/she can critically analyse how it is possible to monetize with data and software\n- He/she can analyze the feasibility of software business models. Student can apply theoretical knowledge and understanding of the data and software business characteristics to collaboratively create a solid lean canvas model for a software start-up.\n", + "relations": [ + { + "type": [ + "Alignment", + "FrameworkAlignment" + ], + "targetName": "think creatively", + "targetUrl": "http://data.europa.eu/esco/skill/d207a30b-2f80-4138-9b77-f88d549b8768", + "targetFramework": "ESCO", + "targetType": "ext:ESCO", + "frameworkUrl": "http://data.europa.eu/esco/concept-scheme/skills" + }, + { + "type": [ + "Alignment", + "FrameworkAlignment" + ], + "targetName": "5.4 Identifying digital competence gaps", + "targetUrl": "https://publications.europa.eu/resource/authority/snb/dcf/860966ekgo", + "targetFramework": "DigiComp", + "targetType": "ext:DigiComp", + "frameworkUrl": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2" + } + ] + }, + + { + "targetUrl": "http://myinstitute.eu/LearningOutcome-3", + "targetName": "Name of DigiComp Competence 2", + "targetCode": "LO2", + "type": [ + "Alignment", + "LearningOutcome" + ], + "targetDescription": "- something else\n", + "relations": [ + { + "type": [ + "Alignment", + "FrameworkAlignment" + ], + "targetName": "3.1 Proficiency Level Foundation 2", + "targetUrl": "http://data.europa.eu/snb/dcf/34v10n662m", + "targetFramework": "DigiComp", + "targetType": "ext:DigiComp", + "frameworkUrl": "https://publications.europa.eu/resource/authority/snb/dcf/25831c2" + } + ] + } + + + + ], + + + "participationType": "onsite or blended", + "assessmentType": "testing", + "learningSetting": "formal", + "identityChecked": true, + "supervisionType": "onsite with identity verification", + "resultDescription": [ + { + "id": "https://example.com/results/ects-nl-NL-A1B2C3", + "type": [ + "ResultDescription" + ], + "valueMax": "10", + "valueMin": "1", + "name": "Final Project Grade", + "requiredValue": "6", + "resultType": "ext:ECTSGradeScore" + } + ] + }, + "result": [ + { + "type": [ + "Result" + ], + "resultDescription": "https://example.com/results/ects-nl-NL-A1B2C3", + "value": "8.5" + } + ] + }, + "credentialSchema": [ + { + "id": "https://purl.imsglobal.org/spec/ob/v3p0/schema/json/ob_v3p0_achievementcredential_schema.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://raw.githubusercontent.com/educredentials/obv3-examples/refs/heads/main/schemas/microcredential.json", + "type": "1EdTechJsonSchemaValidator2019" + }, + { + "id": "https://raw.githubusercontent.com/educredentials/obv3-examples/refs/heads/main/schemas/microcredential_ects.json", + "type": "1EdTechJsonSchemaValidator2019" + } + ] +} From c861dde89d31e23104679b22ba4476cf041220ec Mon Sep 17 00:00:00 2001 From: hamrt Date: Tue, 25 Feb 2025 22:09:06 +0100 Subject: [PATCH 45/45] add context --- json/obv3/contexts/educredential.json | 88 +++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 json/obv3/contexts/educredential.json diff --git a/json/obv3/contexts/educredential.json b/json/obv3/contexts/educredential.json new file mode 100644 index 0000000..96fc98a --- /dev/null +++ b/json/obv3/contexts/educredential.json @@ -0,0 +1,88 @@ +{ + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "EducredentialAchievement": { + "@id": "https://edubadges.nl/static/obv3-glossary.html#EducredentialAchievement", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "assessmentType": { + "@id": "https://edubadges.nl/static/obv3-glossary.html#assessmentType", + "@type": "@vocab", + "@context": { + "testing": "https://edubadges.nl/static/obv3-glossary.html#assessment-type-testing", + "application of a skill": "https://eduabages.nl/static/obv3-glossary.html#assessment-type-application-of-a-skill", + "portfolio": "https://eduabages.nl/static/obv3-glossary.html#assessment-type-portfolio", + "recognition of prior learning": "https://edubadges.nl/static/obv3-glossary.html#assessment-type-recognition-of-prior-learning" + } + }, + "identityChecked": { + "@id": "https://edubadges.nl/static/obv3-glossary.html#identityChecked", + "@type": "xsd:boolean" + }, + "participationType": { + "@id": "https://edubadges.nl/static/obv3-glossary.html#participationType", + "@type": "@vocab", + "@context": { + "online": "https://edubadges.nl/static/obv3-glossary.html#participation-type-online", + "onsite or blended": "https://edubadges.nl/static/obv3-glossary.html#participation-type-onsite-or-blended", + "volunteering": "https://edubadges.nl/static/obv3-glossary.html#participation-type-volunteering", + "work experience": "https://edubadges.nl/static/obv3-glossary.html#participation-type-work-experience", + "personalized learning activities": "https://edubadges.nl/static/obv3-glossary.html#participation-type-personalized-learning-activities" + } + }, + "supervisionType": { + "@id": "https://edubadges.nl/static/obv3-glossary.html#supervisionType", + "@type": "@vocab", + "context": { + "unsupervised with no identity verification": "https://edubadges.nl/static/obv3-glossary.html#supervision-type-unsupervised-with-no-identity-verification", + "supervised with no identity verification": "https://edubadges.nl/static/obv3-glossary.html#supervision-type-supervised-with-no-identity-verification", + "supervised online": "https://edubaadges.nl/static/obv3-glossary.html#supervision-type-supervised-online", + "onsite with identity verification": "https://edubadges.nl/static/obv3-glossary.html#supervision-type-onsite-with-identity-verification" + } + }, + "ECTS": { + "@id": "https://edubadges.nl/static/obv3-glossary.html#ects", + "@type": "xsd:integer" + }, + "timeInvestment": { + "@id": "https://edubadges.nl/static/obv3-glossary.html#timeInvestment", + "@type": "xsd:integer" + }, + "SBU": { + "@id": "https://edubadges.nl/static/obv3-glossary.html#sbu", + "@type": "xsd:integer" + }, + "educationProgramIdentifier": { + "@id": "https://edubadges.nl/static/obv3-glossary.html#educationProgramIdentifier", + "@type": "xsd:number" + }, + "learningOutcome": { + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "relations": { + "@id": "https://purl.imsglobal.org/spec/vc/ob/vocab.html#alignment", + "@container": "@set" + } + } + }, + "FrameworkAlignment": { + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "frameworkUrl": { + "@id": "https://schema.org/targetUrl", + "@type": "https://www.w3.org/2001/XMLSchema#anyURI" + } + } + } + } + } + } +} \ No newline at end of file