├── requirements.txt ├── AUTHORS.md ├── CHANGELOG.md ├── csvtest.csv ├── test2.json ├── LICENSE.md ├── test.json ├── CONTRIBUTING.md ├── .github └── workflows │ └── release.yml ├── README.md ├── test_csdl_creator.py ├── Thingy.v1_0_0.xml ├── xml_convenience.py └── csdl_creator.py /requirements.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Original Contribution: 2 | 3 | * [Matthew Kocurek](//github.com/Yergidy) - HPE - Hewlett Packard Enterprise Restful API Group 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.9.7] - 2022-08-09 4 | - Fixed import and references to 'create_navigation' 5 | 6 | ## [0.9.6] - 2022-06-03 7 | - Added override support for 'description' and 'longDescription' when '!link' annotation is used. 8 | 9 | ## [0.9.5] - 2020-10-19 10 | - Added CSV support 11 | 12 | ## [0.9.0] - 2020-09-27 13 | - Initial release 14 | -------------------------------------------------------------------------------- /csvtest.csv: -------------------------------------------------------------------------------- 1 | ThingType|ShortDesc|LongDesc|Enum1|Enum2 2 | LotsOfStuff|ShortDesc|LongDesc 3 | LotsOfStuff2|ShortDesc|LongDesc 4 | LotsOfStuff3|ShortDesc|LongDesc|Enum1|Enum2|Enum3 5 | LotsOfStuff4|ShortDesc|LongDesc 6 | ThingContainer/ThingKnob|ShortInnerDesc|LongInnerDesc 7 | ThingContainer/ThingButton|HahaButton|HahaButton2 8 | ThingContainer/ThingButton/ButtonColor|ShortInnerDesc|LongInnerDesc -------------------------------------------------------------------------------- /test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "@odata.id": "/redfish/v1/Thingy/1", 3 | "@odata.type": "#Thingy.v1_0_0.Thingy", 4 | "ThingType": "RackMount | Cheap | Expensive | Obsolete | Trendy", 5 | "ThingType!Required": true, 6 | "ThingType!Description": "The type of thingy that this thingy is - really...", 7 | "ThingType!LongDescription": "A long thingy description...", 8 | "IndicatorLED": "Off | Lit | Blinking", 9 | "IndicatorLED!ReadWrite": true, 10 | "Location": {}, 11 | "ThingContainer": { 12 | "ThingKnob": "Twist", 13 | "ThingKnobCount": 6, 14 | "ThingButton": { 15 | "ButtonColor": "Red", 16 | "ButtonColor!Description": "Color of the button.", 17 | "ButtonActuation!Required": true, 18 | "ButtonActuation": 0.34 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020-2024, Contributing Member(s) of Distributed Management Task 4 | Force, Inc.. All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its contributors 17 | may be used to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /test.json: -------------------------------------------------------------------------------- 1 | { 2 | "@odata.id": "/redfish/v1/Thingy/1", 3 | "@odata.type": "#Thingy.v1_0_0.Thingy", 4 | "Id": "1", 5 | "Name": "Big Thingy", 6 | "ThingType": "RackMount | Cheap | Expensive | Obsolete | Trendy", 7 | "ThingType!required": true, 8 | "ThingType!description": "The type of thingy that this thingy is - really...", 9 | "ThingType!longDescription": "A long thingy description...", 10 | "ThingType!enumDescriptions": { 11 | "RackMount": "tmp", 12 | "Cheap": "tmp1", 13 | "Expensive": "tmp2", 14 | "Obsolete": "tmp3", 15 | "Trendy": "tmp4" 16 | }, 17 | "ThingType!enumLongDescriptions": { 18 | "RackMount": "tmp5", 19 | "Cheap": "tmp6", 20 | "Expensive": "tmp7", 21 | "Obsolete": "tmp8", 22 | "Trendy": "tmp9" 23 | }, 24 | "IndicatorLED": "Off | Lit | Blinking", 25 | "IndicatorLED!readonly": false, 26 | "SideFumbling": true, 27 | "SideFumbling!readonly": false, 28 | "Location": {}, 29 | "LotsOfStuff": [ 30 | "Red", 31 | "Blue", 32 | "Purple" 33 | ], 34 | "LotsOfStuff2": [ 35 | 1,2,3 36 | ], 37 | "LotsOfStuff3": [ 38 | "a | b | c" 39 | ], 40 | "LotsOfStuff4": [ 41 | {"My_Name": "Doug"} 42 | ], 43 | "ThingContainer": { 44 | "ThingKnob": "Twist", 45 | "ThingKnobCount": 5, 46 | "ThingButton": { 47 | "ButtonColor": "Red", 48 | "ButtonColor!description": "Color of the button.", 49 | "ButtonActuation!required": true, 50 | "ButtonActuation": 0.34 51 | } 52 | }, 53 | "Status": { 54 | "State": "Enabled", 55 | "Health": "OK" 56 | }, 57 | "PCIeSlots": [{ 58 | "@odata.id": "/redfish/v1/Chassis/1/PCIeSlots" 59 | }], 60 | "PCIeSlots!link": "PCIeDevice", 61 | "Thermal": { 62 | "@odata.id": "/redfish/v1/Chassis/1/Thermal" 63 | }, 64 | "Thermal!link": "Thermal", 65 | "Power": { 66 | "@odata.id": "/redfish/v1/Chassis/1/Power" 67 | }, 68 | "Power!link": "Power", 69 | "Power2": { 70 | "@odata.id": "/redfish/v1/Chassis/1/Power" 71 | }, 72 | "Power2!link": "Power" 73 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Overview 4 | 5 | This repository is maintained by the [DMTF](https://www.dmtf.org/ "https://www.dmtf.org/"). All contributions are reviewed and approved by members of the organization. 6 | 7 | ## Submitting Issues 8 | 9 | Bugs, feature requests, and questions are all submitted in the "Issues" section for the project. DMTF members are responsible for triaging and addressing issues. 10 | 11 | ## Contribution Process 12 | 13 | 1. Fork the repository. 14 | 2. Make and commit changes. 15 | 3. Make a pull request. 16 | 17 | All contributions must adhere to the BSD 3-Clause License described in the LICENSE.md file, and the [Developer Certificate of Origin](#developer-certificate-of-origin). 18 | 19 | Pull requests are reviewed and approved by DMTF members. 20 | 21 | ## Developer Certificate of Origin 22 | 23 | All contributions must adhere to the [Developer Certificate of Origin (DCO)](http://developercertificate.org "http://developercertificate.org"). 24 | 25 | The DCO is an attestation attached to every contribution made by every developer. In the commit message of the contribution, the developer adds a "Signed-off-by" statement and thereby agrees to the DCO. This can be added by using the `--signoff` parameter with `git commit`. 26 | 27 | Full text of the DCO: 28 | 29 | ``` 30 | Developer Certificate of Origin 31 | Version 1.1 32 | 33 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 34 | 35 | Everyone is permitted to copy and distribute verbatim copies of this 36 | license document, but changing it is not allowed. 37 | 38 | 39 | Developer's Certificate of Origin 1.1 40 | 41 | By making a contribution to this project, I certify that: 42 | 43 | (a) The contribution was created in whole or in part by me and I 44 | have the right to submit it under the open source license 45 | indicated in the file; or 46 | 47 | (b) The contribution is based upon previous work that, to the best 48 | of my knowledge, is covered under an appropriate open source 49 | license and I have the right under that license to submit that 50 | work with modifications, whether created in whole or in part 51 | by me, under the same open source license (unless I am 52 | permitted to submit under a different license), as indicated 53 | in the file; or 54 | 55 | (c) The contribution was provided directly to me by some other 56 | person who certified (a), (b) or (c) and I have not modified 57 | it. 58 | 59 | (d) I understand and agree that this project and the contribution 60 | are public and that a record of the contribution (including all 61 | personal information I submit with it, including my sign-off) is 62 | maintained indefinitely and may be redistributed consistent with 63 | this project or the open source license(s) involved. 64 | ``` 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release and Publish 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: 'Version number' 7 | required: true 8 | changes_1: 9 | description: 'Change entry' 10 | required: true 11 | changes_2: 12 | description: 'Change entry' 13 | required: false 14 | changes_3: 15 | description: 'Change entry' 16 | required: false 17 | changes_4: 18 | description: 'Change entry' 19 | required: false 20 | changes_5: 21 | description: 'Change entry' 22 | required: false 23 | changes_6: 24 | description: 'Change entry' 25 | required: false 26 | changes_7: 27 | description: 'Change entry' 28 | required: false 29 | changes_8: 30 | description: 'Change entry' 31 | required: false 32 | jobs: 33 | release_build: 34 | name: Build the release 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v2 38 | with: 39 | token: ${{secrets.GITHUB_TOKEN}} 40 | - name: Build the changelog text 41 | run: | 42 | echo 'CHANGES<> $GITHUB_ENV 43 | echo "## [${{github.event.inputs.version}}] - $(date +'%Y-%m-%d')" >> $GITHUB_ENV 44 | echo "- ${{github.event.inputs.changes_1}}" >> $GITHUB_ENV 45 | if [[ -n "${{github.event.inputs.changes_2}}" ]]; then echo "- ${{github.event.inputs.changes_2}}" >> $GITHUB_ENV; fi 46 | if [[ -n "${{github.event.inputs.changes_3}}" ]]; then echo "- ${{github.event.inputs.changes_3}}" >> $GITHUB_ENV; fi 47 | if [[ -n "${{github.event.inputs.changes_4}}" ]]; then echo "- ${{github.event.inputs.changes_4}}" >> $GITHUB_ENV; fi 48 | if [[ -n "${{github.event.inputs.changes_5}}" ]]; then echo "- ${{github.event.inputs.changes_5}}" >> $GITHUB_ENV; fi 49 | if [[ -n "${{github.event.inputs.changes_6}}" ]]; then echo "- ${{github.event.inputs.changes_6}}" >> $GITHUB_ENV; fi 50 | if [[ -n "${{github.event.inputs.changes_7}}" ]]; then echo "- ${{github.event.inputs.changes_7}}" >> $GITHUB_ENV; fi 51 | if [[ -n "${{github.event.inputs.changes_8}}" ]]; then echo "- ${{github.event.inputs.changes_8}}" >> $GITHUB_ENV; fi 52 | echo "" >> $GITHUB_ENV 53 | echo 'EOF' >> $GITHUB_ENV 54 | - name: Update the changelog 55 | run: | 56 | ex CHANGELOG.md <" 66 | git add CHANGELOG.md 67 | git commit -s -m "${{github.event.inputs.version}} versioning" 68 | git push origin master 69 | - name: Make the release 70 | env: 71 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 72 | run: | 73 | gh release create ${{github.event.inputs.version}} -t ${{github.event.inputs.version}} -n "Changes since last release:"$'\n\n'"$CHANGES" 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redfish Schema Creator 2 | 3 | Copyright 2020 DMTF. All rights reserved. 4 | 5 | ## About 6 | 7 | The most efficient method of crafting a new schema has been to first create a mockup of the data as a JSON payload. Once the data makes sense in that form, sometimes with the addition of a property description list, a schema is then prepared. 8 | 9 | This utility will read a JSON document and using the Redfish conventions and some optional annotations, output a "mostly complete" CSDL Redfish schema file. In addition, as typing the Description and LongDescription text can be cumbersome within a CSDL document, and may have been written in a spreadsheet or other tool prior to schema creation, the utility will also merge a CSV file to incorporate description text into the schema output. 10 | 11 | ## Requirements 12 | 13 | The Redfish Schema Creator requires Python version 3. 14 | 15 | No external packages are required. 16 | 17 | ## Usage 18 | 19 | ``` 20 | usage: csdl_creator.py [-h] [--desc DESC] [--csv CSV] [--toggle] json 21 | 22 | Builds a mostly complete CSDL file from an annotated JSON file and an optional 23 | CSV file. 24 | 25 | positional arguments: 26 | json file to process 27 | 28 | optional arguments: 29 | -h, --help show this help message and exit 30 | --desc DESC sysdescription for identifying logs 31 | --csv CSV csv file of helpful definitions for this file 32 | --toggle print a csv report at the end of the log 33 | ``` 34 | 35 | ## JSON document 36 | 37 | The input document must be valid JSON. The following schema attributes will be determined directly from the JSON payload: 38 | * Schema name (value of @odata.type) 39 | * Property names 40 | * Data types (string, integer, number/float, boolean) 41 | * Array definitions (item type from data type) 42 | * Enum values - string with "|" separators between enum values e.g. "On | Off | Blinking" 43 | * Embedded object hierarchy 44 | * Links objects and navigation properties (object with @odata.id property) 45 | * Actions definitions from Actions object 46 | * References to properties in Resource_v1 can be detected and referenced 47 | * If annotated, include schema reference for any links 48 | 49 | 50 | ### Annotations 51 | 52 | The addition of property-level annotations aid in completing the schema creation. An annotation is constructed using the property name with a "!" separator and annotation name appended. Defined annotations include: 53 | * !description - Description text 54 | * !longDescription - Long Description text 55 | * !link - Schema name for link target (populate property and schema reference) 56 | * !readonly - mark property as R/W instead of R/O (default is R/O) 57 | * !required - mark property as Required 58 | 59 | 60 | ### Sample annotated JSON 61 | 62 | ```json 63 | { 64 | "@odata.id": "/redfish/v1/Thingy/1", 65 | "@odata.type": "#Thingy.v1_0_0.Thingy", 66 | "Id": "1", 67 | "Name": "Big Thingy", 68 | "ThingType": "RackMount | Cheap | Expensive | Obsolete | Trendy", 69 | "ThingType!required": true, 70 | "ThingType!description": "The type of thingy that this thingy is - really...", 71 | "IndicatorLED": "Off | Lit | Blinking", 72 | "IndicatorLED!readonly": false, 73 | "Location": { }, 74 | "Status": { 75 | "State": "Enabled", 76 | "Health": "OK" 77 | }, 78 | "PCIeSlots": { 79 | "@odata.id": "/redfish/v1/Chassis/1/PCIeSlots" 80 | }, 81 | "PCIeSlots!link": "PCIeDevice", 82 | "Thermal": { 83 | "@odata.id": "/redfish/v1/Chassis/1/Thermal" 84 | }, 85 | "Thermal!link": "Thermal", 86 | "Power": { 87 | "@odata.id": "/redfish/v1/Chassis/1/Power" 88 | } 89 | } 90 | ``` 91 | 92 | ## Supplemental CSV file 93 | 94 | A comma-separated variable (CSV) file can be specified as input to provide description text, using the following: 95 | 96 | `||` 97 | 98 | The property name could also provide a JSON path `object/property` or similar style to allow inclusion of properties within embedded objects. 99 | 100 | If a property is an EnumType, you may apply descriptions to each Enum by adding further values to a given row: 101 | 102 | `|||||...` 103 | 104 | ## XML header file 105 | 106 | An XML file with the default comment block header, follow existing Redfish schema, with keyword for schema name replacement. 107 | 108 | 109 | ## Schema output 110 | 111 | The utility will process the JSON document and CSV file, if provided, to produce a Redfish CSDL schema file with incomplete data notated for user to provide, and reasonable defaults taken for other attributes that cannot be determined. 112 | 113 | Default property attributes: 114 | * Properties marked as read-only 115 | * Properties non-nullable 116 | * Schema version and namespace is v1.0.0 117 | 118 | Default inclusions: 119 | * Actions block 120 | * Capabilities block 121 | * OEM block 122 | 123 | Default text: 124 | * "***TBD***" strings for Description and LongDescription text if not found in payload or CSV 125 | * "***TBD***" names for linked schemas and types if not provided in annotations. 126 | * Generic description/longDescription for links based on existing Redfish style 127 | 128 | ## Release Process 129 | 130 | 1. Go to the "Actions" page 131 | 2. Select the "Release and Publish" workflow 132 | 3. Click "Run workflow" 133 | 4. Fill out the form 134 | 5. Click "Run workflow" 135 | -------------------------------------------------------------------------------- /test_csdl_creator.py: -------------------------------------------------------------------------------- 1 | # Copyright Notice: 2 | # Copyright 2017-2020 DMTF. All rights reserved. 3 | # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Schema-Creator/blob/main/LICENSE.md 4 | 5 | import pytest 6 | from csdl_creator import CsdlFile, database_builder, create_navigation, create_property, create_enum_property 7 | 8 | TEST_FULL_DATA = { 9 | "@odata.id": "/redfish/v1/Thingy/1", 10 | "@odata.type": "#Thingy.v1_0_0.Thingy", 11 | "Id": "1", 12 | "Name": "Big Thingy", 13 | "ThingType": "RackMount | Cheap | Expensive | Obsolete | Trendy", 14 | "ThingType!Required": True, 15 | "ThingType!Description": "The type of thingy that this thingy is - really...", 16 | "ThingType!LongDescription": "A long thingy description...", 17 | "IndicatorLED": "Off | Lit | Blinking", 18 | "IndicatorLED!ReadWrite": True, 19 | "Location": { }, 20 | "ThingContainer" : {"ThingKnob" : "Twist", "ThingKnobCount": 5, "ThingButton": {"ButtonColor": "Red", "ButtonColor@Description": "Color of the button.", "ButtonActuation@Required": True,"ButtonActuation": .34}}, 21 | "Status": { 22 | "State": "Enabled", 23 | "Health": "OK" 24 | }, 25 | "PCIeSlots": { 26 | "@odata.id": "/redfish/v1/Chassis/1/PCIeSlots" 27 | }, 28 | "PCIeSlots@Link": "PCIeDevice", 29 | "Thermal": { 30 | "@odata.id": "/redfish/v1/Chassis/1/Thermal" 31 | }, 32 | "Thermal@Link": "Thermal", 33 | "Power": { 34 | "@odata.id": "/redfish/v1/Chassis/1/Power" 35 | } 36 | } 37 | 38 | @pytest.fixture 39 | def csdlfile_class(): 40 | test_class = CsdlFile(TEST_DATA) 41 | return test_class 42 | 43 | @pytest.fixture 44 | def built_csdlfile_class(): 45 | test_class = CsdlFile(TEST_DATA) 46 | test_class.build_csdl() 47 | return test_class 48 | 49 | class TestPropertyCreators: 50 | """Strongly tests the property entry creation functions""" 51 | def test_nav_tbd_schema(self): 52 | entry = create_navigation('ThingyHolder') 53 | assert(entry.tag == "NavigationProperty" and entry.get("Type") == "TBD.TBD" and entry.get("Nullable") == 'false' and entry.get("Name") == "ThingyHolder") 54 | 55 | def test_nav_named_schema(self): 56 | entry = create_navigation('ThingyHolder', schema_name="SchemaName") 57 | assert(entry.tag == "NavigationProperty" and entry.get("Type") == "SchemaName.SchemaName" and entry.get("Nullable") == 'false' and entry.get("Name") == "ThingyHolder") 58 | 59 | def test_nav_description(self): 60 | description = create_navigation('ThingyHolder', description="This is a description.") 61 | default_description = create_navigation('ThingyHolder') 62 | assert((description.tag == "NavigationProperty" and description.find("OData.Description").get("String") == "This is a description.") and\ 63 | (default_description.tag == "NavigationProperty" and default_description.find("OData.Description").get("String") == "A link to ThingyHolder")) 64 | 65 | def test_nav_long_description(self): 66 | long_description = create_navigation('ThingyHolder', long_description="This is a long description.") 67 | default_long_description = create_navigation('ThingyHolder') 68 | assert((long_description.tag == "NavigationProperty" and long_description.find("OData.LongDescription").get("String") == "This is a long description.") and\ 69 | (default_long_description.tag == "NavigationProperty" and default_long_description.find("OData.LongDescription").get("String") == "This property shall be a link to a resource collection of type ThingyHolder.")) 70 | 71 | def test_property_defaults(self): 72 | entry = create_property("ThingProperty", "A.Type") 73 | annotations = entry.findall("Annotation") 74 | permissions = description = long_description = None 75 | 76 | for annotation in annotations: 77 | if annotation.get("Term") == "OData.Permissions": 78 | permissions = annotation 79 | elif annotation.get("Term") == "OData.Description": 80 | description = annotation 81 | elif annotation.get("Term") == "OData.LongDescription": 82 | long_description = annotation 83 | 84 | assert(entry.tag == "Property" and entry.get("Name") == "ThingProperty" and entry.get("Type") == "A.Type" and\ 85 | permissions.get("EnumMember") == "OData.Permission/Read" and description.get("String") == "TBD" and long_description.get("String") == "TBD") 86 | 87 | def test_permissions(self): 88 | entry = create_property("ThingProperty", "A.Type", read_write=True) 89 | annotations = entry.findall("Annotation") 90 | permissions = None 91 | 92 | for annotation in annotations: 93 | if annotation.get("Term") == "OData.Permissions": 94 | permissions = annotation 95 | 96 | assert(entry.tag == "Property" and permissions.get("EnumMember") == "OData.Permission/ReadWrite") 97 | 98 | def test_description(self): 99 | entry = create_property("ThingProperty", "A.Type", description="A description.") 100 | annotations = entry.findall("Annotation") 101 | description = None 102 | 103 | for annotation in annotations: 104 | if annotation.get("Term") == "OData.Description": 105 | description = annotation 106 | 107 | assert(entry.tag == "Property" and description.get("String") == "A description.") 108 | 109 | def test_long_description(self): 110 | entry = create_property("ThingProperty", "A.Type", long_description="A long description.") 111 | annotations = entry.findall("Annotation") 112 | long_description = None 113 | 114 | for annotation in annotations: 115 | if annotation.get("Term") == "OData.LongDescription": 116 | long_description = annotation 117 | 118 | assert(entry.tag == "Property" and long_description.get("String") == "A long description.") 119 | 120 | def test_enum_property(self): 121 | entry = create_enum_property("AnEnum", ["A", "B", "C", "D"]) 122 | annotations = entry.findall("Member") 123 | 124 | assert(all([True if item.get("Name") in ["A", "B", "C", "D"] else False \ 125 | for item in annotations])) 126 | 127 | class TestDatabaseBuilder: 128 | """Strongly tests the database building function""" 129 | def test_builder(self): 130 | final_dict = {} 131 | database_builder(TEST_FULL_DATA, final_dict) 132 | assert("ThingKnob" in final_dict["ThingContainer"] and\ 133 | "ButtonColor" in final_dict["ThingContainer"]["ThingButton"] and\ 134 | final_dict["ThingContainer"]["ThingButton"]["ButtonColor"]["description"]=="Color of the button." and\ 135 | final_dict["ThingContainer"]["ThingButton"]["ButtonActuation"]["required"]==True) 136 | 137 | def test_description_annotation(self): 138 | final_dict = {} 139 | database_builder(TEST_FULL_DATA, final_dict) 140 | 141 | assert(final_dict['ThingType']['description'] == "The type of thingy that this thingy is - really...") 142 | 143 | def test_longdescription_annotation(self): 144 | final_dict = {} 145 | database_builder(TEST_FULL_DATA, final_dict) 146 | 147 | assert(final_dict['ThingType']['longdescription'] == "A long thingy description...") 148 | 149 | def test_link_annotation(self): 150 | final_dict = {} 151 | database_builder(TEST_FULL_DATA, final_dict) 152 | 153 | assert(final_dict['PCIeSlots']['link'] == "PCIeDevice") 154 | 155 | def test_readwrite_annotation(self): 156 | final_dict = {} 157 | database_builder(TEST_FULL_DATA, final_dict) 158 | 159 | assert(final_dict['IndicatorLED']['readwrite'] == True) 160 | 161 | def test_required_annotation(self): 162 | final_dict = {} 163 | database_builder(TEST_FULL_DATA, final_dict) 164 | 165 | assert(final_dict['ThingType']['required'] == True) 166 | 167 | def test_enum_annotation(self): 168 | final_dict = {} 169 | database_builder(TEST_FULL_DATA, final_dict) 170 | 171 | assert(all([True if item in ["RackMount", "Cheap", "Expensive", "Obsolete", "Trendy"] else False \ 172 | for item in final_dict['ThingType']['value']])) 173 | 174 | def test_enum_w_empty_annotation(self): 175 | final_dict = {} 176 | database_builder({"ThingType": "|RackMount | Cheap | Expensive | Obsolete | Trendy|"}, final_dict) 177 | 178 | assert(all([True if item in ["RackMount", "Cheap", "Expensive", "Obsolete", "Trendy"] else False \ 179 | for item in final_dict['ThingType']['value']])) 180 | 181 | class TestCsdlFile: 182 | def test_name(self): 183 | test_class = CsdlFile(TEST_FULL_DATA) 184 | assert(test_class.name == 'Thingy.v1_0_0') 185 | 186 | def test_build_csdl(self): 187 | test_class = CsdlFile(TEST_FULL_DATA) 188 | test_class.build_csdl() 189 | 190 | assert(test_class._annotation_database is not {}) 191 | -------------------------------------------------------------------------------- /Thingy.v1_0_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /xml_convenience.py: -------------------------------------------------------------------------------- 1 | # Copyright Notice: 2 | # Copyright 2017-2020 DMTF. All rights reserved. 3 | # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Schema-Creator/blob/main/LICENSE.md 4 | 5 | from xml.etree.ElementTree import Element, SubElement, Comment, tostring 6 | 7 | def get_valid_csdl_identifier(name): 8 | """ 9 | Replace characters that are invalid in CSDL with appropriate valid ones 10 | """ 11 | new_name = name.replace('-', '_').replace(':', '.').replace('"', '').replace('\'', '') 12 | dots = new_name.split('.') 13 | # dots = [string.capwords(s, '_') for s in dots] 14 | 15 | # return '.'.join(dots).replace('_', '') 16 | return '.'.join(dots) 17 | 18 | def add_import(xml_node, import_value, alias, namespace=None): 19 | """ 20 | Add import stament node into XML. 21 | :param xml_node:Parent node to which 'import' node must be added. 22 | :param import_value: The name of the imported file 23 | :param alias: Optional alias for the imported file. 24 | """ 25 | uri = 'http://redfish.dmtf.org/schemas/v1/' + import_value + "_v1.xml" 26 | for ref in xml_node: 27 | if 'Reference' not in str(ref.tag): 28 | continue 29 | uri_old = ref.attrib.get('Uri') 30 | if uri_old == uri: 31 | return None 32 | namespace = import_value if not namespace else namespace 33 | return add_reference(xml_node, uri, namespace, alias) 34 | 35 | 36 | def add_reference(xml_node, uri, namespace, alias=None, version=None): 37 | """ 38 | Add "edmx:Reference" node to XML. 39 | :param xml_node:Parent node to which reference node must be added. 40 | :param uri: URI which will be added as the reference 41 | :param namespace: XML namespace or namespaces 42 | :param alias: Optional alias 43 | """ 44 | if uri in ['', None]: 45 | uri = 'http://redfish.dmtf.org/schemas/v1/' + namespace + "_v1.xml" 46 | ref = Element('edmx:Reference') 47 | ref.set('Uri', uri) 48 | include = SubElement(ref, 'edmx:Include') 49 | if alias is not None: 50 | include.set('Alias', alias) 51 | if version: 52 | include.set('Namespace', namespace + '.' + version) 53 | else: 54 | include.set('Namespace', namespace) 55 | if xml_node is not None: 56 | xml_node.insert(0, ref) 57 | return ref 58 | 59 | 60 | def add_references(xml_node, uri, entries): 61 | """ 62 | Add "edmx:Reference" node to XML. 63 | :param xml_node: Parent node to which reference node must be added. 64 | :param uri: URI which will be added as the reference 65 | :param entries: Dictionary of namespace-alias pairs 66 | """ 67 | ref = SubElement(xml_node, 'edmx:Reference') 68 | ref.set('Uri', uri) 69 | for namespace, alias in entries.items(): 70 | include = SubElement(ref, 'edmx:Include') 71 | if alias not in [None, '']: 72 | include.set('Alias', alias) 73 | include.set('Namespace', namespace) 74 | return xml_node 75 | 76 | 77 | def add_list_reference(xml_node, uri, namespace, alias): 78 | ref = SubElement(xml_node, 'edmx:Reference') 79 | ref.set('Uri', uri) 80 | ref.set('Namespace' , namespace + '.xml') 81 | include = SubElement(ref, 'edmx:Include') 82 | if alias is not None: 83 | include.set('Alias', alias) 84 | if namespace is not None: 85 | include.set('Namespace', namespace) 86 | return xml_node 87 | 88 | 89 | 90 | def add_CSDL_Headers(is_singleton=False): 91 | """ 92 | Add standard CSDL tags/references which are common across all the CSDL 93 | XML files. 94 | :param xml_node:Parent node to which headers must be added. 95 | """ 96 | xml_node = Element('edmx:Edmx') 97 | xml_node.set('Version', '4.0') 98 | xml_node.set('xmlns:edmx', 'http://docs.oasis-open.org/odata/ns/edmx') 99 | 100 | add_reference(xml_node, 101 | 'http://docs.oasis-open.org/odata/odata/v4.0/errata03/csd01/complete/vocabularies/Org.OData.Core.V1.xml', 102 | 'Org.OData.Core.V1', 'OData') 103 | add_reference(xml_node, 104 | 'http://docs.oasis-open.org/odata/odata/v4.0/errata03/csd01/complete/vocabularies/Org.OData.Capabilities.V1.xml', 105 | 'Org.OData.Capabilities.V1', 'Capabilities') 106 | add_references(xml_node, 107 | 'http://redfish.dmtf.org/schemas/v1/RedfishExtensions_v1.xml', 108 | {'RedfishExtensions.v1_0_0': 'Redfish'}) 109 | if is_singleton: 110 | nspaces = {'RedfishExtensions.v1_0_0': 'Redfish', 'Validation.v1_0_0': 'Validation'} 111 | return xml_node 112 | 113 | 114 | def add_annotation(xml_node, key_values): 115 | """ 116 | Add node of type "Annotation" to the XML 117 | :param xml_node: Node to which Annotations have to be addeed. 118 | :param key_values: Key value pairs which will be used to create the 119 | annotation entries. 120 | """ 121 | if xml_node is None: 122 | return Element('Annotation', key_values) 123 | annotation = SubElement(xml_node, 'Annotation') 124 | for key in key_values.keys(): 125 | annotation.set(key, key_values[key]) 126 | return annotation 127 | 128 | 129 | def add_collection_annotation(xml_node, key_values, record_props): 130 | annotation = SubElement(xml_node, 'Annotation') 131 | for key in key_values.keys(): 132 | annotation.set(key, key_values[key]) 133 | record = SubElement(annotation, 'Record') 134 | for key in record_props.keys(): 135 | record_property = SubElement(record, "PropertyValue") 136 | record_property.set('Property', key) 137 | record_property.set('Bool', record_props[key]) 138 | return annotation 139 | 140 | 141 | def createExtensionsXML(name, types, xml_node = None, owner='TBD', release='TBD'): 142 | if xml_node is None: 143 | xml_node = Element('edmx:Edmx') 144 | xml_node.set('Version', '4.0') 145 | xml_node.set('xmlns:edmx', 'http://docs.oasis-open.org/odata/ns/edmx') 146 | 147 | add_reference(xml_node, 148 | 'http://docs.oasis-open.org/odata/odata/v4.0/errata03/csd01/complete/vocabularies/Org.OData.Core.V1.xml', 149 | 'Org.OData.Core.V1', 'OData') 150 | 151 | extension_data_services_node = SubElement( 152 | xml_node, 'edmx:DataServices') 153 | extension_schema_node = SubElement(extension_data_services_node, 'Schema') 154 | extension_schema_node.set("xmlns", "http://docs.oasis-open.org/odata/ns/edm") 155 | extension_schema_node.set("Namespace", name + 'Extensions.v1_0_0') 156 | 157 | add_annotation(extension_schema_node, {"Term": "Redfish.OwningEntity", "String": owner}) 158 | add_annotation(extension_schema_node, {"Term": "OData.LongDescription", "String": "The CSDL Terms, Type Definitions, and Enumerations defined in this schema section shall be interpreted as defined in RFC6020."}) 159 | else: 160 | extension_schema_node = xml_node 161 | 162 | return xml_node 163 | 164 | 165 | def createCollectionXML(name, prefix='', xml_node=None, owner='TBD', release='TBD'): 166 | collection_name = name + "Collection" 167 | 168 | if xml_node is None: 169 | collection_xml_root = add_CSDL_Headers(None) 170 | add_reference( 171 | collection_xml_root, "http://redfish.dmtf.org/schemas/v1/Resource_v1.xml", "Resource.v1_0_0", None) 172 | add_reference( 173 | collection_xml_root, "http://redfish.dmtf.org/schemas/v1/" + prefix + name + "_v1.xml", prefix + name, name.split('.')[-1]) 174 | 175 | collection_data_services_node = SubElement( 176 | collection_xml_root, 'edmx:DataServices') 177 | else: 178 | collection_xml_root = xml_node 179 | collection_data_services_node = xml_node.find('./') 180 | 181 | 182 | collection_schema_node = SubElement(collection_data_services_node, 'Schema') 183 | collection_schema_node.set("xmlns", "http://docs.oasis-open.org/odata/ns/edm") 184 | collection_schema_node.set("Namespace", prefix + collection_name) 185 | 186 | add_annotation(collection_schema_node, {"Term": "Redfish.OwningEntity", "String": owner}) 187 | 188 | collection_target = SubElement(collection_schema_node, "EntityType") 189 | collection_target.set('Name', collection_name.split('.')[-1]) 190 | collection_target.set('BaseType', "Resource.v1_0_0.ResourceCollection") 191 | 192 | add_annotation(collection_target, {"Term": "OData.Description", 193 | "String": "A Collection of " + name + " resource instances." 194 | }) 195 | add_annotation(collection_target, {"Term": "OData.LongDescription", 196 | "String": "A Collection of " + name + " resource instances." 197 | }) 198 | add_collection_annotation(collection_target, {"Term": 199 | "Capabilities.InsertRestrictions"}, {"Insertable": "false"}) 200 | add_collection_annotation(collection_target, {"Term": 201 | "Capabilities.UpdateRestrictions"}, {"Updatable": "false"}) 202 | add_collection_annotation(collection_target, {"Term": 203 | "Capabilities.DeleteRestrictions"}, {"Deletable": "false"}) 204 | 205 | nav_prop = SubElement(collection_target, 'NavigationProperty') 206 | nav_prop.set('Name', 'Members') 207 | listname = name.split('.')[-1] 208 | # should this be without the namespace at all (errata) 209 | nav_prop.set('Type', 'Collection(' + listname + '.' + listname + ')') 210 | add_annotation(nav_prop, {'Term':'OData.Permissions', 211 | 'EnumMember':'OData.Permissions/Read'}) 212 | add_annotation(nav_prop, {'Term':'OData.Description', 'String':'Contains members of this collection.'}) 213 | add_annotation(nav_prop, {'Term':'OData.LongDescription', 'String':'Contains members of this collection.'}) 214 | add_annotation(nav_prop, {'Term':'OData.AutoExpandReferences'}) 215 | add_annotation(nav_prop, {'Term': 'Redfish.Required'}) 216 | return collection_xml_root 217 | 218 | 219 | def create_xml_base(csdlname, prefix='', xml_node = None, owner='TBD', release='TBD'): 220 | if xml_node is None: 221 | main_node = add_CSDL_Headers(True) 222 | add_reference(main_node, 223 | "http://redfish.dmtf.org/schemas/v1/Resource_v1.xml", 224 | "Resource.v1_0_0", None ) 225 | entity_node = None 226 | # add refs to main, then add data services node 227 | data_services_node = Element('edmx:DataServices') 228 | else: 229 | main_node = xml_node 230 | data_services_node = xml_node.find('./') 231 | schema_node = SubElement(data_services_node, 'Schema') 232 | schema_node.set( 233 | 'Namespace', prefix + csdlname + ".v1_0_0") 234 | schema_node.set( 235 | 'xmlns', 'http://docs.oasis-open.org/odata/ns/edm') 236 | add_annotation(schema_node, {"Term": "Redfish.OwningEntity", "String": owner}) 237 | add_annotation(schema_node, {"Term": "Redfish.Release", "String": release}) 238 | # add in first schema 239 | schema_node_og = Element('Schema') 240 | schema_node_og.set('Namespace', prefix + csdlname) 241 | schema_node_og.set( 242 | 'xmlns', 'http://docs.oasis-open.org/odata/ns/edm') 243 | add_annotation(schema_node_og, {"Term": "Redfish.OwningEntity", "String": owner}) 244 | data_services_node.insert(0, schema_node_og) 245 | 246 | schema1_entity = SubElement(schema_node_og, 'EntityType') 247 | schema1_entity.set('Name', csdlname.split('.')[-1]) 248 | schema1_entity.set('BaseType', 'Resource.v1_0_0.Resource') 249 | schema1_entity.set( 250 | 'Abstract', 'true') 251 | add_annotation(schema1_entity, { 252 | 'Term': 'OData.Description', 'String': 'Parameters for {}.'.format(csdlname)}) 253 | add_annotation(schema1_entity, { 254 | 'Term': 'OData.LongDescription', 'String': 'Parameters for {}.'.format(csdlname)}) 255 | add_collection_annotation(schema1_entity, {"Term": 256 | "Capabilities.InsertRestrictions"}, {"Insertable": "false"}) 257 | add_collection_annotation(schema1_entity, {"Term": 258 | "Capabilities.UpdateRestrictions"}, {"Updatable": "false"}) 259 | add_collection_annotation(schema1_entity, {"Term": 260 | "Capabilities.DeleteRestrictions"}, {"Deletable": "false"}) 261 | main_node.append(data_services_node) 262 | return main_node, schema_node, data_services_node -------------------------------------------------------------------------------- /csdl_creator.py: -------------------------------------------------------------------------------- 1 | # Copyright Notice: 2 | # Copyright 2017-2020 DMTF. All rights reserved. 3 | # License: BSD 3-Clause License. For full text see link: https://github.com/DMTF/Redfish-Schema-Creator/blob/main/LICENSE.md 4 | 5 | import re 6 | import sys 7 | import csv 8 | import json 9 | import argparse 10 | import xml_convenience 11 | import xml.dom.minidom 12 | import xml.etree.ElementTree as etree 13 | # from xml.etree.ElementTree import Element, SubElement, Comment, tostring 14 | 15 | REGEX_TYPE = "#([a-zA-z]*.v\\d_\\d_\\d)" 16 | 17 | RESOURCE_TYPES = ['Id', 'Description', 'Name', 'UUID', 'Links', 'Oem', 'OemObject', 'ItemOrCollection', 'Item', 'ReferenceableMember', 'Resource', 'ResourceCollection', 'Status', 'State', 'Health', 'ResetType', 'Identifier', 'Location', 'IndicatorLED', 'PowerState'] 18 | 19 | RESOURCE_PROPERTIES = ['Description', 'Name', 'Id'] 20 | 21 | RESOURCE_COLLECTION_PROPERTIES = ['Description', 'Name', 'Oem'] 22 | 23 | CSDL_HEADER_TEMPLATE = """ 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | """ 34 | 35 | def database_builder(annotated_json, csv={}): 36 | """Transforms annotated json into a dictionary database 37 | 38 | :param annotated_json: Annotated json to turn into a dictionary. 39 | :type annotated_json: dict 40 | :param data_base: The dictionary containing the final database. Pass this as an empty dict. 41 | :type data_base: dict 42 | """ 43 | data_base = {} 44 | for key, value in annotated_json.items(): 45 | # Use ! to delineate Schema Annotation 46 | if '!' in key: 47 | prop, annotation = tuple(key.split('!', 1)) 48 | else: 49 | prop, annotation = key, None 50 | # Skip all @ items 51 | if '@' in key or prop in ['', None]: 52 | continue 53 | data_base[prop] = {} if prop not in data_base else data_base[prop] 54 | if annotation is None: 55 | if isinstance(value, list): 56 | data_base[prop]["items"] = { 57 | "type": list(set([type(x) for x in value])) 58 | } 59 | data_base[prop]["type"] = "array" 60 | value = value[0] 61 | if isinstance(value, dict): 62 | sub_csv = {k.split('/', 1)[1]: s for k, s in csv.items() if prop in k and '/' in k} 63 | data_base[prop]["properties"] = database_builder(value, sub_csv) 64 | elif isinstance(value, str) and '|' in value: 65 | #For enum values 66 | value = [val.strip() for val in value.split('|') if val] 67 | data_base[prop]["enum"] = value 68 | data_base[prop]["value"] = value 69 | else: 70 | data_base[prop][annotation] = value 71 | for prop in data_base: 72 | if prop in csv: 73 | print(prop) 74 | data_base[prop]['description'] = csv[prop][0] 75 | data_base[prop]['longDescription'] = csv[prop][1] 76 | if data_base[prop].get("enum"): 77 | csv_enum = csv[prop][2:] 78 | print(csv_enum, prop) 79 | if data_base[prop].get("enumDescriptions") is None: 80 | data_base[prop]["enumDescriptions"] = {} 81 | data_base[prop]["enumDescriptions"].update({e: d for e, d in zip(data_base[prop]["value"], csv_enum)}) 82 | 83 | 84 | return data_base 85 | 86 | 87 | def create_navigation(property_name, schema_name="TBD", description=None, longDescription=None, collection=False, **kwargs): 88 | """Creates a navigation entry to add to the CSDL schema.""" 89 | description = description if description else "A link to {}".format(property_name) 90 | longDescription = longDescription if longDescription else "This property shall be a link to a resource collection of type {}.".format(property_name) 91 | 92 | if collection: 93 | navegation_entry = etree.Element("NavigationProperty",\ 94 | attrib={"Name": property_name, "Type": "Collection({}.{})".format(schema_name, schema_name), "Nullable": "false"}) 95 | else: 96 | navegation_entry = etree.Element("NavigationProperty",\ 97 | attrib={"Name": property_name, "Type": "{}.{}".format(schema_name, schema_name), "Nullable": "false"}) 98 | 99 | etree.SubElement(navegation_entry, "Annotation", 100 | attrib={"Term":"OData.Permissions", "EnumMember": "OData.Permission/Read"} ) 101 | etree.SubElement(navegation_entry, "Annotation", attrib={"Term": "OData.Description", "String": description}) 102 | etree.SubElement(navegation_entry, "Annotation", attrib={"Term": "OData.LongDescription", "String": longDescription}) 103 | if collection: 104 | etree.SubElement(navegation_entry, "Annotation", {'Term':'OData.AutoExpandReferences'}) 105 | 106 | return navegation_entry 107 | 108 | 109 | def create_property(property_name, property_type, readonly=True, required=False, type=None, description=None, longDescription=None, items=None): 110 | """Creates a base property to build from""" 111 | description = description if description else "TBD" 112 | longDescription = longDescription if longDescription else "TBD" 113 | readonly = "OData.Permission/Read" if readonly else "OData.Permission/ReadWrite" 114 | 115 | property_string = "ComplexType" if complex else "Property" 116 | 117 | added_property = etree.Element("Property", attrib={"Name": property_name, "Type": property_type, "Nullable": "false"}) 118 | etree.SubElement(added_property, "Annotation", attrib={"Term": "OData.Permissions", "EnumMember": readonly}) 119 | etree.SubElement(added_property, "Annotation", attrib={"Term": "OData.Description", "String": description}) 120 | etree.SubElement(added_property, "Annotation", attrib={"Term": "OData.LongDescription", "String": longDescription}) 121 | if required: 122 | etree.SubElement(added_property, "Annotation", attrib={"Term": "Redfish.Required"}) 123 | 124 | return added_property 125 | 126 | 127 | def create_complex_property(property_name, base_type=None, description=None, longDescription=None, readonly=True, type=None, **kwargs): 128 | """Create a complex property type""" 129 | description = description if description else "TBD" 130 | longDescription = longDescription if longDescription else "TBD" 131 | added_property = etree.Element("ComplexType", attrib={"Name": property_name, "Nullable": "false"}) 132 | if base_type: 133 | added_property.attrib["BaseType"] = base_type 134 | etree.SubElement(added_property, "Annotation", attrib={"Term": "OData.Description", "String": description}) 135 | etree.SubElement(added_property, "Annotation", attrib={"Term": "OData.LongDescription", "String": longDescription}) 136 | etree.SubElement(added_property, "Annotation", attrib={"Term": "OData.AdditionalProperties", "Bool": "false"}) 137 | 138 | return added_property 139 | 140 | 141 | def create_enum_property(property_name, enum_values, enum_description=None, enum_longDescription=None): 142 | """Creates a property of enum type to add to the CSDL schema.""" 143 | enum_property_entry = etree.Element("EnumType", attrib={"Name": property_name}) 144 | 145 | for value in enum_values: 146 | value_entry = etree.SubElement(enum_property_entry, "Member", attrib={"Name": value}) 147 | description = enum_description.get(value) if enum_description else "TBD" 148 | longDescripton = enum_longDescription.get(value) if enum_longDescription else "TBD" 149 | etree.SubElement(value_entry, "Annotation", attrib={"Term": "OData.Description", "String": description}) 150 | 151 | return enum_property_entry 152 | 153 | type_conversion = { 154 | str: "Edm.String", 155 | float: "Edm.Decimal", 156 | bool: "Edm.Boolean", 157 | int: "Edm.Int64", 158 | "string": "Edm.String", 159 | "number": "Edm.Decimal", 160 | "boolean": "Edm.Boolean", 161 | "integer": "Edm.Int64", 162 | } 163 | 164 | def create_property_w_type(property_name, value=None, my_type=None, **kwargs): 165 | """Creates a base property using create_property based on the type value is""" 166 | entry = None 167 | my_type = type(value) if my_type is None else my_type 168 | if isinstance(value, list) or my_type in ["array", list] or kwargs.get('type') == "array": 169 | my_type = kwargs['items']['type'][0] 170 | entry = create_property(property_name, "Collection({})".format(type_conversion.get(my_type, 'TBD')), **kwargs) 171 | elif my_type in type_conversion: 172 | entry = create_property(property_name, type_conversion[my_type], **kwargs) 173 | else: 174 | entry = create_property(property_name, "TBD", **kwargs) 175 | 176 | return entry 177 | 178 | 179 | class CsdlFile: 180 | """CSDL file that is created from the passed JSON""" 181 | def __init__(self, annotated_json, inherited_prop_list=[], csv=None): 182 | self.csdl = None 183 | self.main_csdl = None 184 | self.annotated_json = annotated_json 185 | # if the JSON input file is not a json-schema, build a database from the mockup 186 | if "$schema" in self.annotated_json: 187 | self._annotation_database = self.annotated_json["properties"] 188 | self._name = annotated_json['title'] 189 | 190 | for key in list(self._annotation_database): 191 | if "@" in key: 192 | del self._annotation_database[key] 193 | else: 194 | self._annotation_database = database_builder(self.annotated_json, csv) 195 | self._name = annotated_json['@odata.type'] 196 | 197 | for item in inherited_prop_list: 198 | if item in self._annotation_database: 199 | del self._annotation_database[item] 200 | 201 | def __str__(self): 202 | return str(self.csdl) 203 | 204 | @property 205 | def name(self): 206 | """Returns the simple version of the @odata.type""" 207 | match = re.match(REGEX_TYPE, self._name) 208 | return match.group(1) if match else self._name 209 | 210 | @name.setter 211 | def name(self, name): 212 | """Sets the name property""" 213 | self._name = name 214 | 215 | def init_csdl(self): 216 | """Builds the header and the root of th CSDL file.""" 217 | # self.csdl = etree.Element("Edmx", nsmap={"edmx":"http://docs.oasis-open.org/odata/ns/edmx"}, attrib={"Version": "4.0"})#etree.XML(CSDL_HEADER_TEMPLATE.format(self.name)) 218 | self.main_csdl, self.csdl, _services = xml_convenience.create_xml_base(self.name.split('.')[0]) 219 | self.entity = etree.Element('EntityType', { 220 | 'Name': self.name.split('.')[0], 221 | 'BaseType': '.'.join([self.name.split('.')[0]]*2) 222 | }) 223 | self.csdl.append(self.entity) 224 | 225 | def build_csdl(self): 226 | for key, descriptors in self._annotation_database.items(): 227 | self.build_csdl_node(self.entity, key, descriptors) 228 | 229 | def build_csdl_node(self, entry, key, descriptors): 230 | """Builds the csdl file from the annotated json database""" 231 | kwargs = {} 232 | if "description" in descriptors: 233 | kwargs["description"] = descriptors["description"] 234 | if "longDescription" in descriptors: 235 | kwargs["longDescription"] = descriptors["longDescription"] 236 | if key in RESOURCE_TYPES: 237 | entry.append(create_property(key, "Resource.{}".format(key))) 238 | return 239 | if "required" in descriptors: 240 | kwargs["required"] = True 241 | if "readonly" in descriptors: 242 | kwargs["readonly"] = False 243 | if "type" in descriptors: 244 | # JSON schema file 245 | kwargs["type"] = descriptors["type"] 246 | if "items" in descriptors: 247 | # JSON schema file 248 | kwargs["items"] = descriptors["items"] 249 | value = descriptors.get("value", None) 250 | if isinstance(value, dict) or "properties" in descriptors: 251 | # kwargs["base_name"] = "Resource.%s" % key 252 | if "$ref" in descriptors: 253 | # for jsonschema 254 | pass 255 | if "link" in descriptors: 256 | kwargs = {} 257 | kwargs['schema_name'] = descriptors['link'] 258 | if 'description' in descriptors: 259 | kwargs['description'] = descriptors['description'] 260 | if 'longDescription' in descriptors: 261 | kwargs['longDescription'] = descriptors['longDescription'] 262 | if not any([descriptors['link'] in x.attrib.get('Uri', '').split('/')[-1] for x in self.main_csdl]): 263 | my_node = xml_convenience.add_reference(None, None, descriptors['link']) 264 | self.main_csdl.insert(3, my_node) 265 | if descriptors.get("type") == "array": 266 | kwargs['collection'] = True 267 | entry.append(create_navigation(key, **kwargs)) 268 | else: 269 | complex_prop = create_complex_property(key, **kwargs) 270 | if descriptors.get("type") == "array": 271 | entry.append(create_property(key, "Collection(%s.%s)" % (self.name, key), **kwargs)) 272 | else: 273 | entry.append(create_property(key, "%s.%s" % (self.name, key), **kwargs)) 274 | for key, descriptors in descriptors["properties"].items(): 275 | self.build_csdl_node(complex_prop, key, descriptors) 276 | self.csdl.append(complex_prop) 277 | elif "enum" in descriptors: 278 | if descriptors.get("type") == "array": 279 | entry.append(create_property(key, "Collection(%s.%s)" % (self.name, key), **kwargs)) 280 | else: 281 | entry.append(create_property(key, "%s.%s" % (self.name, key), **kwargs)) 282 | self.csdl.append(create_enum_property(key, descriptors.get("enum"), descriptors.get("enumDescriptions", None), descriptors.get("enumLongDescriptions", None))) 283 | else: 284 | entry.append(create_property_w_type(key, value, **kwargs)) 285 | 286 | def add_schema_link(self): 287 | #TODO: Add schema link to CSDL 288 | pass 289 | 290 | 291 | def main(): 292 | """ Main function """ 293 | argget = argparse.ArgumentParser(description='Builds a mostly complete CSDL file from an annotated JSON file and an optional CSV file.') 294 | 295 | # Create Tool Arguments 296 | argget.add_argument('json', type=str, help='file to process') 297 | argget.add_argument('--desc', type=str, default='No desc', help='sysdescription for identifying logs') 298 | argget.add_argument('--csv', type=str, help='csv file of helpful definitions for this file') 299 | argget.add_argument('--toggle', action='store_true', help='print a csv report at the end of the log') 300 | 301 | args = argget.parse_args() 302 | 303 | # Get File 304 | file_name = args.json 305 | try: 306 | with open(file_name) as fle: 307 | file_data = fle.read() 308 | json_data = json.loads(file_data) 309 | except json.JSONDecodeError: 310 | sys.stderr.write("Unable to parse JSON file supplied.") 311 | return 1 312 | except Exception: 313 | sys.stderr.write("Problem getting file provided") 314 | return 1 315 | 316 | csv_dict = {} 317 | 318 | if args.csv: 319 | with open(args.csv) as f: 320 | csv_reader = csv.reader(f, delimiter='|') 321 | for line in csv_reader: 322 | csv_dict[line[0]] = line[1:] 323 | print(csv_dict) 324 | 325 | 326 | csdl = CsdlFile(json_data, RESOURCE_PROPERTIES, csv=csv_dict) 327 | csdl.init_csdl() 328 | csdl.build_csdl() 329 | output_xml = csdl.name+'.xml' 330 | 331 | with open(output_xml, 'w') as output: 332 | xml_string = etree.tostring(csdl.main_csdl, encoding="unicode", method="xml") 333 | xml_obj = xml.dom.minidom.parseString(xml_string) 334 | xml_string = xml_obj.toprettyxml() 335 | 336 | pretty_xml_as_string = xml_string 337 | pretty_xml_as_string_new = '' 338 | 339 | # input(pretty_xml_as_string) 340 | # post process Annotations 341 | priority_tags = ["xmlns", "xmlns:edmx", "Name", "Term", "Property", "Type", "Namespace", "EnumMember", "String", "Bool"] 342 | for line in pretty_xml_as_string.split('\n'): 343 | priority_tag_dict = {} 344 | other_tags = [] 345 | allresults = re.findall('[a-zA-Z:]+?=".+?"', line) 346 | tokened_line = re.sub('[a-zA-Z:]+?=".+?"', 'xxToken', line) 347 | for tag in allresults: 348 | tag_name, tag_content = tuple(tag.split('=', 1)) 349 | if tag_name not in priority_tags: 350 | other_tags.append(tag_name) 351 | priority_tag_dict[tag_name] = tag 352 | 353 | for tag_name in priority_tags + other_tags: 354 | if tag_name in priority_tag_dict: 355 | tokened_line = tokened_line.replace('xxToken', priority_tag_dict[tag_name], 1) 356 | 357 | tokened_line = tokened_line.replace('"', '"') 358 | pretty_xml_as_string_new += tokened_line + '\n' 359 | output.write(CSDL_HEADER_TEMPLATE.format(csdl.name) + pretty_xml_as_string_new) 360 | #csdl.csdl.getroottree().write(output_xml, xml_declaration=True, encoding="UTF-8", pretty_print=True) 361 | 362 | return 0 363 | 364 | if __name__ == '__main__': 365 | sys.exit(main()) 366 | --------------------------------------------------------------------------------