├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── MAINTENANCE.md ├── README.md ├── bin └── travis-run-test.sh ├── composer.json ├── schemata.info.yml ├── schemata.permissions.yml ├── schemata.routing.yml ├── schemata.services.yml ├── schemata_json_schema ├── schemata_json_schema.info.yml ├── schemata_json_schema.services.yml └── src │ ├── Annotation │ └── TypeMapper.php │ ├── Normalizer │ ├── hal │ │ ├── ComplexDataDefinitionNormalizer.php │ │ ├── DataDefinitionNormalizer.php │ │ ├── DataReferenceDefinitionNormalizer.php │ │ ├── FieldDefinitionNormalizer.php │ │ ├── ListDataDefinitionNormalizer.php │ │ ├── ReferenceListTrait.php │ │ └── SchemataSchemaNormalizer.php │ ├── json │ │ ├── ComplexDataDefinitionNormalizer.php │ │ ├── DataDefinitionNormalizer.php │ │ ├── DataReferenceDefinitionNormalizer.php │ │ ├── FieldDefinitionNormalizer.php │ │ ├── JsonNormalizerBase.php │ │ ├── ListDataDefinitionNormalizer.php │ │ └── SchemataSchemaNormalizer.php │ └── jsonapi │ │ ├── ComplexDataDefinitionNormalizer.php │ │ ├── DataDefinitionNormalizer.php │ │ ├── FieldDefinitionNormalizer.php │ │ ├── JsonApiNormalizerBase.php │ │ ├── ListDataDefinitionNormalizer.php │ │ ├── RelationshipFieldDefinitionNormalizer.php │ │ └── SchemataSchemaNormalizer.php │ ├── Plugin │ ├── Type │ │ └── TypeMapperPluginManager.php │ └── schemata_json_schema │ │ └── type_mapper │ │ ├── DateTime8601TypeMapper.php │ │ ├── EmailTypeMapper.php │ │ ├── EntityReferenceTypeMapper.php │ │ ├── FallbackTypeMapper.php │ │ ├── FilterFormatTypeMapper.php │ │ ├── TimestampTypeMapper.php │ │ ├── TypeMapperBase.php │ │ └── TypeMapperInterface.php │ └── SchemataJsonSchemaServiceProvider.php ├── src ├── Controller │ └── Controller.php ├── Encoder │ └── JsonSchemaEncoder.php ├── Normalizer │ └── NormalizerBase.php ├── Routing │ └── Routes.php ├── Schema │ ├── NodeSchema.php │ ├── Schema.php │ └── SchemaInterface.php ├── SchemaFactory.php ├── SchemaUrl.php └── SchemataServiceProvider.php └── tests ├── phpcs-ruleset.xml.dist ├── phpcs.xml └── src ├── Functional ├── SchemataBrowserTestBase.php └── ValidateSchemaTest.php └── Kernel └── SchemaFactoryTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Adapted from https://github.com/Gizra/og/blob/8.x-1.x/.travis.yml 2 | language: php 3 | sudo: false 4 | 5 | php: 6 | - 7.1 7 | - 7.0 8 | - 7.2 9 | 10 | env: 11 | - TEST_SUITE=CODE_QUALITY 12 | - TEST_SUITE=8.4.x 13 | - TEST_SUITE=8.5.x 14 | 15 | # Only run the coding standards check once. 16 | matrix: 17 | exclude: 18 | - php: 7.0 19 | env: TEST_SUITE=CODE_QUALITY 20 | - php: 7.2 21 | env: TEST_SUITE=CODE_QUALITY 22 | allow_failures: 23 | - php: 7.2 24 | 25 | mysql: 26 | database: schemata 27 | username: root 28 | encoding: utf8 29 | 30 | before_script: 31 | # Remove Xdebug as we don't need it and it causes "PHP Fatal error: Maximum 32 | # function nesting level of '256' reached." 33 | # We also don't care if that file exists or not on PHP 7. 34 | - phpenv config-rm xdebug.ini || true 35 | 36 | # Make sure Composer is up to date. 37 | - composer self-update 38 | 39 | # Remember the current directory for later use in the Drupal installation. 40 | - MODULE_DIR=$(pwd) 41 | 42 | # Install Composer dependencies for Schemata. 43 | # @todo not needed for phpunit tests. 44 | - composer install 45 | 46 | # Navigate out of module directory to prevent blown stack by recursive module 47 | # lookup. 48 | - cd .. 49 | 50 | # Create database. 51 | - mysql -e 'create database schemata' 52 | 53 | # Download Drupal 8 core. Skip this for the coding standards test. 54 | - test ${TEST_SUITE} == "CODE_QUALITY" || travis_retry git clone --branch $TEST_SUITE --depth 1 https://git.drupal.org/project/drupal.git 55 | 56 | # Remember the Drupal installation path. 57 | - DRUPAL_DIR=$(pwd)/drupal 58 | 59 | # Install Composer dependencies for core. Skip this for the coding standards test. 60 | - test ${TEST_SUITE} == "CODE_QUALITY" || composer install --working-dir=$DRUPAL_DIR 61 | 62 | # Add Schemata dependent libraries. They must be installed as part of Drupal for autoloading.. 63 | # This needs to be manually updated whenever composer.json is updated. 64 | - test ${TEST_SUITE} == "CODE_QUALITY" || composer require --dev league/json-guard:^1.0 league/json-reference:^1.0 --working-dir=$DRUPAL_DIR || true 65 | 66 | # Start a web server on port 8888 in the background. 67 | - test ${TEST_SUITE} == "CODE_QUALITY" || nohup php -S localhost:8888 --docroot $DRUPAL_DIR > /dev/null 2>&1 & 68 | 69 | # Wait until the web server is responding. 70 | - test ${TEST_SUITE} == "CODE_QUALITY" || until curl -s localhost:8888; do true; done > /dev/null 71 | 72 | # Export web server URL for browser tests. 73 | - export SIMPLETEST_BASE_URL=http://localhost:8888 74 | 75 | # Export database variable for kernel tests. 76 | - export SIMPLETEST_DB=mysql://root:@127.0.0.1/schemata 77 | 78 | script: DRUPAL_DIR=$DRUPAL_DIR MODULE_DIR=$MODULE_DIR $MODULE_DIR/bin/travis-run-test.sh $TEST_SUITE 79 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /MAINTENANCE.md: -------------------------------------------------------------------------------- 1 | # Maintenance 2 | 3 | This documents the activities for maintenance of the Schemata project. 4 | 5 | ## Resources 6 | 7 | * Use Drupal.org for project homepage, documentation, canonical code hosting, 8 | support requests, and security. 9 | * Use Github for development via Pull Requests and CI processes. 10 | 11 | ## Routine Activities 12 | 13 | * Use `composer outdated --direct` and `composer update ` 14 | to ensure dependencies are up-to-date. 15 | * Update the composer require --dev line in .travis.yml so the Travis CI 16 | testing pulls the updated library versions. 17 | * Use `composer run-script phpcbf` to fix many classes of phpcs violation. 18 | 19 | ## Policy/Guidelines 20 | 21 | * Treat the README as both a deeper orientation of the project than is provided 22 | by the Drupal.org project page, as well as an initial entrypoint to the project 23 | for Github explorers. 24 | * Thank code contributors for any contribution first, and again after committing 25 | or merging the contribution. 26 | * If tests fail do not merge. 27 | * Do not commit composer.lock 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Schemata 2 | 3 | > Facilitate generation of schema definitions of Drupal 8 data models. 4 | 5 | [![GitHub tag](https://img.shields.io/github/tag/phase2/schemata.svg)](https://github.com/phase2/schemata) [![Website](https://img.shields.io/badge/website-drupal.org-blue.svg)](https://www.drupal.org/project/schemata) [![Build Status](https://travis-ci.org/phase2/schemata.svg?branch=8.x-1.x)](https://travis-ci.org/phase2/schemata) 6 | 7 | A schema is a declarative description of structured data that prescribes the 8 | rules for how that data can be created. Schemas are commonly used to provision 9 | relational storage, to generate forms or other user interfaces, to generate 10 | client library code, or to validate data. 11 | 12 | Schemata supports the creation of provider modules that create schemas to 13 | describe the different entities in a Drupal site (such as Nodes, 14 | Taxonomy Terms, and Users) as they are rendered in Drupal REST responses. 15 | This project ensures your Drupal site is able to deliver self-documenting 16 | machine descriptions of your API payloads, driven by the same configuration that 17 | Drupal uses to build forms and validate entities. 18 | 19 | In addition to the more abstract developer module named "Schemata", there is 20 | a reference implementation of a schema provider: Schemata JSON Schema, which 21 | provides [JSON Schema](http://json-schema.org/) support to define entities 22 | delivered in Drupal Core's JSON and HAL JSON REST formats, and the contrib 23 | module and format [JSON API](https://www.drupal.org/project/jsonapi). 24 | 25 | ## What has a Schema? 26 | 27 | All Content Entity Types and Bundles have a schema automatically via the 28 | Schemata module. Core support for this kind of structured description of 29 | configuration does not exist yet. 30 | 31 | ## Where is the Schema? 32 | 33 | Schemata are accessed via regular routes. Once enabled, Schemata resources are 34 | found at `/schemata/{entity_type}/{bundle?}`. These resources are dynamically 35 | generated based on your entity definitions, which means any change to fields on 36 | an Entity will automatically be reflected in the schema. 37 | 38 | ## Requirements 39 | 40 | Drupal 8.3 is required for Schemata's generated schemas to validate your REST 41 | responses. 42 | 43 | Schemata-the-module is a dependency for the sub-modules of the project: 44 | 45 | * **Schemata JSON Schema**: A serializer which processes Schemata schema objects 46 | into [JSON Schema v4](http://json-schema.org). Describes the output of content 47 | entities via the core JSON, HAL and JSON API serializers. 48 | 49 | From a "product" standpoint, JSON Schema is the value, and Schemata 50 | the technical dependency. Only install Schemata if you plan to install a Schema 51 | provider module that depends on it. 52 | 53 | ## Architecture 54 | 55 | The Schemata project contains the Schemata and Schemata JSON Schema modules. 56 | The Schemata module provides routes to retrieve a serialized, machine-readable 57 | schema. The schemas are assembled dynamically leveraging the 58 | [Typed Data API](https://www.drupal.org/node/1794140) and other site configuration. 59 | The schema for each entity type (and bundle) is exposed in the manner of a 60 | REST resource, accessible via GET requests designating the appropriate `_format` 61 | parameter to select a schema type and `_describes` to target a particular entity 62 | representation format supported by Drupal REST. 63 | 64 | In order to serialize the Schema object, the serializer must be able to support 65 | implementations of the Drupal\schemata\Schema\SchemaInterface class. At this 66 | time, the only serializer support for Schemata is within this project, you can 67 | see an example of this in the packaged submodule **Schemata JSON Schema**. 68 | 69 | ## Usage 70 | 71 | You can obtain the schema either making an HTTP request or by using the 72 | programmatic API. 73 | 74 | Each schema type format should be contained in its own module. Enable the 75 | module for the format that you need first. For instance: 76 | 77 | ``` 78 | drush en -y schemata_json_schema 79 | ``` 80 | 81 | Finally you need to grant permission to access the data models to roles that 82 | need it. (`access schemata data models`) 83 | 84 | ### Request 85 | 86 | Create a request against 87 | `/schemata/{entity_type}/{bundle}?_format={output_format}&_describes={described_format}` 88 | For instance: 89 | 90 | * `/schemata/node/article?_format=schema_json&_describes=hal_json` 91 | * `/schemata/user?_format=schema_json&_describes=api_json` (omit the bundle 92 | if the entity type has no bundles). 93 | 94 | ### Programmatically 95 | 96 | ```php 97 | // Input variables. 98 | $entity_type_id = 'node'; 99 | $bundle = 'article'; 100 | $output_format = 'schema_json'; 101 | $described_format = 'api_json'; 102 | 103 | // Services. 104 | $schema_factory = \Drupal::service('schemata.schema_factory'); 105 | $serializer = \Drupal::service('serializer'); 106 | 107 | // Generate a Schema object. 108 | $schema = $schema_factory->create($entity_type_id, $bundle); 109 | 110 | // Render the schema as a string conforming to the selecting schema type. 111 | $format = $output_format . ':' . $described_format; 112 | $serializer->serialize($schema, $format); 113 | ``` 114 | 115 | ### Related Projects 116 | 117 | Use the [Docson](https://www.drupal.org/project/docson) module to visualize 118 | the JSON Schemas generated by the Schemata module. 119 | 120 | Use the [OpenAPI](https://www.drupal.org/project/openapi) module to generate 121 | a Swagger v2 API definition specification, which can in turn be used by ecosystem 122 | of Swagger-based tools. For example, the [Swagger Tools page](https://swagger.io/tools/) 123 | and [ReDoc](https://rebilly.github.io/ReDoc/). 124 | 125 | ## Contribute 126 | 127 | * Security reports should follow [Drupal.org security reporting procedures] 128 | (https://www.drupal.org/node/101494). 129 | * Other bugs and issues should be filed in the [Issue Queue](https://www.drupal.org/project/issues/schemata) 130 | * Code contributions should be submitted as a [Pull Request in Github](https://github.com/phase2/schemata) 131 | 132 | ## URLs 133 | 134 | * [Homepage]() 135 | * [Development](https://github.com/phase2/schemata) 136 | 137 | ## Maintainers 138 | 139 | Adam Ross a.k.a. Grayside 140 | 141 | ## Contributors 142 | 143 | Creation of this module was sponsored by 144 | [Norwegian Cruise Line](https://www.drupal.org/norwegian-cruise-line). 145 | 146 | Fubhy's work on [GraphQL](https://www.drupal.org/project/graphql) was a great 147 | help in early architecture of this project. Thank you to 148 | [Fubhy](https://www.drupal.org/u/fubhy) and the GraphQL sponsors. 149 | 150 | ## F.A.Q. 151 | 152 | ### Will there be a Drupal 7 backport? 153 | 154 | This module can be summarized as follows: 155 | 156 | A Drupal 8 subsystem (routing) and a Symfony subsystem (Serialization) use a 157 | touchy Drupal 8 subsystem (Typed Data) to describe the output of another 158 | Drupal 8 subsystem (Entity). 159 | 160 | It gets worse--the use cases of this module are most applicable when said entity 161 | output comes by way of the routing and Serialization systems. 162 | 163 | It does not make sense to discuss a backport unless you first backport large 164 | swathes of Drupal 8. 165 | 166 | ### Why not use some other module? 167 | 168 | [Self Documenting REST API](https://www.drupal.org/project/rest_api_doc) 169 | produces webpage reports about REST resources on the site. This project might 170 | eventually compete with that if it builds out support for the 171 | [Swagger specification](http://swagger.io/). 172 | 173 | [Field Report](https://www.drupal.org/project/field_report) enhances the core 174 | reports about fields and content types. This has some similarity with this 175 | project, but the reports provided by Field Report are for people orienting on 176 | a site. This project produces schemas that can be used for machine integrations 177 | or feeding into other report generation systems. 178 | -------------------------------------------------------------------------------- /bin/travis-run-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run either PHPUnit tests or CODE_QUALITY tests on Travis CI, depending 4 | # on the passed in parameter. 5 | # 6 | # Adapted from https://github.com/Gizra/og/blob/8.x-1.x/scripts/travis-ci/run-test.sh 7 | 8 | case "$1" in 9 | CODE_QUALITY) 10 | cd $MODULE_DIR 11 | echo "ENSURE DEVELOPMENT TOOLS" 12 | composer install 13 | echo "VALIDATE COMPOSER.JSON FILE" 14 | composer validate --no-check-lock --no-check-publish --no-interaction 15 | echo "RUN CODE QUALITY CHECKS" 16 | composer run-script quality 17 | exit $? 18 | ;; 19 | *) 20 | echo "RUN PHPUNIT TESTS" 21 | ln -sv $MODULE_DIR $DRUPAL_DIR/modules/schemata 22 | cd $DRUPAL_DIR 23 | ./vendor/bin/phpunit -c ./core/phpunit.xml.dist $MODULE_DIR/tests 24 | exit $? 25 | esac 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drupal/schemata", 3 | "description": "Facilitate generation of schema definitions of Drupal 8 data models.", 4 | "type": "drupal-module", 5 | "keywords": [ "Drupal", "json", "schema" ], 6 | "homepage": "https://drupal.org/project/schemata", 7 | "support": { 8 | "issues": "https://drupal.org/project/issues/schemata", 9 | "source": "https://cgit.drupalcode.org/schemata" 10 | }, 11 | "license": "GPL-2.0+", 12 | "authors": [ 13 | { 14 | "name": "Adam Ross", 15 | "email": "grayside@gmail.com" 16 | } 17 | ], 18 | "prefer-stable": true, 19 | "minimum-stability": "dev", 20 | "config": { 21 | "sort-packages": true 22 | }, 23 | "scripts": { 24 | "phpcs": "phpcs --standard=tests/phpcs.xml", 25 | "phpcbf": "phpcbf --standard=tests/phpcs.xml", 26 | "lint": "parallel-lint -e php,module,install,profile,theme,inc --exclude vendor/ --blame .", 27 | "quality": [ 28 | "@lint", 29 | "@phpcs" 30 | ] 31 | }, 32 | "repositories": [ 33 | { 34 | "type": "composer", 35 | "url": "https://packages.drupal.org/8" 36 | } 37 | ], 38 | "require": {}, 39 | "require-dev": { 40 | "drupal/coder": "^8.2", 41 | "jakub-onderka/php-parallel-lint": "^0.9.2", 42 | "league/json-guard": "^1.0", 43 | "league/json-reference": "^1.0" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /schemata.info.yml: -------------------------------------------------------------------------------- 1 | name: Schemata 2 | type: module 3 | description: Provide schema definitions of Drupal entities for type validation, code generation, and documentation. 4 | core: 8.x 5 | package: Web services 6 | dependencies: 7 | - serialization 8 | -------------------------------------------------------------------------------- /schemata.permissions.yml: -------------------------------------------------------------------------------- 1 | access schemata data models: 2 | title: 'Access the different data models' 3 | -------------------------------------------------------------------------------- /schemata.routing.yml: -------------------------------------------------------------------------------- 1 | route_callbacks: 2 | - '\Drupal\schemata\Routing\Routes::routes' 3 | -------------------------------------------------------------------------------- /schemata.services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | serializer.encoder.json_schema: 3 | class: Drupal\schemata\Encoder\JsonSchemaEncoder 4 | tags: 5 | - { name: encoder, priority: 10, format: 'schema_json' } 6 | 7 | # Assemble Schemas based on entity type definitions. As a standalone class, 8 | # the SchemaFactory can be pulled in to custom menu routes, drush commands, 9 | # and REST plugins. The SchemaFactory::create() creates instances of 10 | # Drupal\schemata\Schema\SchemaInterface. 11 | schemata.schema_factory: 12 | class: Drupal\schemata\SchemaFactory 13 | arguments: 14 | - '@logger.channel.schemata' 15 | - '@entity_type.manager' 16 | - '@entity_type.bundle.info' 17 | - '@typed_data_manager' 18 | - '@config.typed' 19 | 20 | # Create a log channel for this module. This simplifies logging setup code in 21 | # this code that will do logging. As a tradeoff, it also adds this fairly 22 | # specific logging channel to the system for all requests. 23 | logger.channel.schemata: 24 | parent: logger.channel_base 25 | arguments: ['schemata'] 26 | -------------------------------------------------------------------------------- /schemata_json_schema/schemata_json_schema.info.yml: -------------------------------------------------------------------------------- 1 | name: Schemata in JSON Schema 2 | type: module 3 | description: Provides a data models for entity types and bundles in JSON schema format. 4 | core: 8.x 5 | package: Web services 6 | dependencies: 7 | - schemata 8 | -------------------------------------------------------------------------------- /schemata_json_schema/schemata_json_schema.services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | # ---------------------------------------------------------------------------- 3 | # - NORMALIZERS for each format. 4 | # ---------------------------------------------------------------------------- 5 | # - 1. JSON format. 6 | # ---------------------------------------------------------------------------- 7 | 8 | # References should be converted to other schema resources for the type. 9 | # This priority ensures the DataReferenceDefinition is handled before 10 | # DataDefinition. Since it is orthogonal to ComplexDataDefinition, they share 11 | # the same priority. 12 | serializer.normalizer.data_reference_definition.schema_json.json: 13 | class: Drupal\schemata_json_schema\Normalizer\json\DataReferenceDefinitionNormalizer 14 | arguments: ['@entity_type.manager'] 15 | tags: 16 | - { name: normalizer, priority: 25 } 17 | 18 | # Normalize complex data properties. 19 | # This priority ensures the ComplexDataDefinition is handled before 20 | # DataDefinition. Since it is orthagonal to DataReferenceDefinition, they 21 | # share the same priority. 22 | serializer.normalizer.complex_data_definition.schema_json.json: 23 | class: Drupal\schemata_json_schema\Normalizer\json\ComplexDataDefinitionNormalizer 24 | tags: 25 | - { name: normalizer, priority: 20 } 26 | 27 | # Field definitions are a variant of List definitions, with additional access 28 | # to the particular schema and configuration pieces from the field system. As 29 | # a subclass of ListDataDefinitionInterface, FieldDefinitionInterface needs a 30 | # higher priority. 31 | serializer.normalizer.field_definition.schema_json.json: 32 | class: Drupal\schemata_json_schema\Normalizer\json\FieldDefinitionNormalizer 33 | tags: 34 | - { name: normalizer, priority: 30 } 35 | 36 | # If the typed data definition is a list (as most are somewhere along the 37 | # property hierarchy) this triggers the recursion to the next layer. 38 | serializer.normalizer.list_data_definition.schema_json.json: 39 | class: Drupal\schemata_json_schema\Normalizer\json\ListDataDefinitionNormalizer 40 | tags: 41 | - { name: normalizer, priority: 20 } 42 | 43 | # Typed data definitions in general can take many forms. This handles final items. 44 | serializer.normalizer.data_definition.schema_json.json: 45 | class: Drupal\schemata_json_schema\Normalizer\json\DataDefinitionNormalizer 46 | tags: 47 | - { name: normalizer, priority: 10 } 48 | 49 | # This is the main JSON Schema normalizer. 50 | serializer.normalizer.schema.schema_json.json: 51 | class: Drupal\schemata_json_schema\Normalizer\json\SchemataSchemaNormalizer 52 | tags: 53 | - { name: normalizer, priority: 10 } 54 | # ---------------------------------------------------------------------------- 55 | # - 2. JSON API format. 56 | # ---------------------------------------------------------------------------- 57 | serializer.normalizer.complex_data_definition.schema_json.jsonapi: 58 | class: Drupal\schemata_json_schema\Normalizer\jsonapi\ComplexDataDefinitionNormalizer 59 | tags: 60 | - { name: normalizer, priority: 20 } 61 | serializer.normalizer.field_definition.schema_json.jsonapi: 62 | class: Drupal\schemata_json_schema\Normalizer\jsonapi\FieldDefinitionNormalizer 63 | tags: 64 | - { name: normalizer, priority: 30 } 65 | serializer.normalizer.relationship_field_definition.schema_json.jsonapi: 66 | class: Drupal\schemata_json_schema\Normalizer\jsonapi\RelationshipFieldDefinitionNormalizer 67 | arguments: ['@plugin.manager.field.field_type'] 68 | tags: 69 | - { name: normalizer, priority: 35 } 70 | serializer.normalizer.list_data_definition.schema_json.jsonapi: 71 | class: Drupal\schemata_json_schema\Normalizer\jsonapi\ListDataDefinitionNormalizer 72 | tags: 73 | - { name: normalizer, priority: 20 } 74 | serializer.normalizer.data_definition.schema_json.jsonapi: 75 | class: Drupal\schemata_json_schema\Normalizer\jsonapi\DataDefinitionNormalizer 76 | tags: 77 | - { name: normalizer, priority: 10 } 78 | serializer.normalizer.schema.schema_json.jsonapi: 79 | class: Drupal\schemata_json_schema\Normalizer\jsonapi\SchemataSchemaNormalizer 80 | tags: 81 | - { name: normalizer, priority: 10 } 82 | # ---------------------------------------------------------------------------- 83 | # - 3. HAL+JSON format. 84 | # ---------------------------------------------------------------------------- 85 | # Note: The HAL+JSON version of the Data Reference normalizer depends on the 86 | # HAL module and is in registered in SchemataHalServiceProvider. 87 | 88 | # Normalize complex data properties for HAL. 89 | # This services is primarily used to short-circuit merging normalization of 90 | # data references to the schema root. 91 | serializer.normalizer.complex_data_definition.schema_json.hal_json: 92 | class: Drupal\schemata_json_schema\Normalizer\hal\ComplexDataDefinitionNormalizer 93 | tags: 94 | - { name: normalizer, priority: 25 } 95 | 96 | # HAL version of FieldDefinition. 97 | # This services is primarily used to short-circuit merging normalization of 98 | # data references to the schema root. 99 | serializer.normalizer.field_definition.schema_json.hal_json: 100 | class: Drupal\schemata_json_schema\Normalizer\hal\FieldDefinitionNormalizer 101 | tags: 102 | - { name: normalizer, priority: 35 } 103 | 104 | # HAL version of the ListDataDefinition. 105 | # This services is primarily used to short-circuit merging normalization of 106 | # data references to the schema root. 107 | serializer.normalizer.list_data_definition.schema_json.hal_json: 108 | class: Drupal\schemata_json_schema\Normalizer\hal\ListDataDefinitionNormalizer 109 | tags: 110 | - { name: normalizer, priority: 25 } 111 | serializer.normalizer.schema.schema_json.hal_json: 112 | class: Drupal\schemata_json_schema\Normalizer\hal\SchemataSchemaNormalizer 113 | tags: 114 | - { name: normalizer, priority: 15 } 115 | serializer.normalizer.data_definition.schema_json.hal_json: 116 | class: Drupal\schemata_json_schema\Normalizer\hal\DataDefinitionNormalizer 117 | tags: 118 | - { name: normalizer, priority: 10 } 119 | 120 | plugin.manager.schemata_json_schema.type_mapper: 121 | class: Drupal\schemata_json_schema\Plugin\Type\TypeMapperPluginManager 122 | arguments: ['@container.namespaces', '@module_handler'] 123 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Annotation/TypeMapper.php: -------------------------------------------------------------------------------- 1 | isReferenceField($entity, $context)) { 33 | return parent::normalize($entity, $format, $context); 34 | } 35 | 36 | // Not overriding the $context['parent'] here allows trickle-down of 37 | // top-level field labels. However, we do need some of the field settings. 38 | $context['settings'] = $entity->getSettings(); 39 | return $this->serializer->normalize( 40 | $entity->getPropertyDefinition('entity'), 41 | $format, 42 | $context 43 | ); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/hal/DataDefinitionNormalizer.php: -------------------------------------------------------------------------------- 1 | linkManager = $link_manager; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function normalize($entity, $format = NULL, array $context = []) { 53 | /* @var $entity \Drupal\Core\TypedData\DataReferenceDefinitionInterface */ 54 | if (!$this->validateEntity($entity)) { 55 | return []; 56 | } 57 | 58 | // Collect data about the reference field. 59 | $parentProperty = $this->extractPropertyData($context['parent'], $context); 60 | $property = $this->extractPropertyData($entity, $context); 61 | $target_type = $entity->getConstraint('EntityType'); 62 | $target_bundles = isset($context['settings']['handler_settings']['target_bundles']) ? 63 | $context['settings']['handler_settings']['target_bundles'] : []; 64 | 65 | // Build the relation URI, which is used as the property key. 66 | $field_uri = $this->linkManager->getRelationUri( 67 | $context['entityTypeId'], 68 | // Drupal\Core\Entity\Entity::bundle() returns Entity Type ID by default. 69 | isset($context['bundleId']) ? $context['bundleId'] : $context['entityTypeId'], 70 | $context['name'], 71 | $context 72 | ); 73 | 74 | // From the root of the schema object, build out object references. 75 | $normalized = [ 76 | '_links' => [ 77 | $field_uri => [ 78 | '$ref' => '#/definitions/linkArray', 79 | ], 80 | ], 81 | '_embedded' => [ 82 | $field_uri => [ 83 | 'type' => 'array', 84 | 'items' => [], 85 | ], 86 | ], 87 | ]; 88 | 89 | // Add title and description to relation definition. 90 | if (isset($parentProperty['title'])) { 91 | $normalized['_links'][$field_uri]['title'] = $parentProperty['title']; 92 | $normalized['_embedded'][$field_uri]['title'] = $parentProperty['title']; 93 | } 94 | if (isset($parentProperty['description'])) { 95 | $normalized['_links'][$field_uri]['description'] = $parentProperty['description']; 96 | } 97 | 98 | // Add Schema resource references. 99 | $item = &$normalized['_embedded'][$field_uri]['items']; 100 | if (empty($target_bundles)) { 101 | $generated_url = SchemaUrl::fromOptions( 102 | $this->format, 103 | $this->describedFormat, 104 | $target_type 105 | )->toString(TRUE); 106 | $item['$ref'] = $generated_url->getGeneratedUrl(); 107 | } 108 | elseif (count($target_bundles) == 1) { 109 | $generated_url = SchemaUrl::fromOptions( 110 | $this->format, 111 | $this->describedFormat, 112 | $target_type, 113 | reset($target_bundles) 114 | )->toString(TRUE); 115 | $item['$ref'] = $generated_url->getGeneratedUrl(); 116 | } 117 | elseif (count($target_bundles) > 1) { 118 | $refs = []; 119 | foreach ($target_bundles as $bundle) { 120 | $generated_url = SchemaUrl::fromOptions( 121 | $this->format, 122 | $this->describedFormat, 123 | $target_type, 124 | $bundle 125 | )->toString(TRUE); 126 | $refs[] = [ 127 | '$ref' => $generated_url->getGeneratedUrl(), 128 | ]; 129 | } 130 | 131 | $item['anyOf'] = $refs; 132 | } 133 | 134 | return ['properties' => $normalized]; 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/hal/FieldDefinitionNormalizer.php: -------------------------------------------------------------------------------- 1 | isReferenceField($entity)) { 19 | return parent::normalize($entity, $format, $context); 20 | } 21 | 22 | // Unlike 23 | // Drupal\schemata_json_schema\Normalizer\json\ListDataDefinitionNormalizer, 24 | // this does not return the nested value into the property's 'items' 25 | // attribute. Instead it returns the normalized reference definition to be 26 | // merged at the normalized object root. This means the item definition 27 | // referred to below can choose to add new properties, required values, and 28 | // so on. 29 | $context['parent'] = $entity; 30 | return $this->serializer->normalize( 31 | $entity->getItemDefinition(), 32 | $format, 33 | $context 34 | ); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/hal/SchemataSchemaNormalizer.php: -------------------------------------------------------------------------------- 1 | '#/definitions/linkObject', 48 | ]; 49 | $items['type'] = [ 50 | '$ref' => '#/definitions/linkObject', 51 | ]; 52 | 53 | $normalized['properties']['_links'] = [ 54 | 'title' => 'HAL Links', 55 | 'description' => 'Object of links with the rels as the keys', 56 | 'type' => 'object', 57 | 'properties' => $items, 58 | ]; 59 | 60 | if (!empty($normalized['properties']['_embedded'])) { 61 | $items = $normalized['properties']['_embedded']; 62 | $normalized['properties']['_embedded'] = [ 63 | 'title' => 'HAL Embedded Resource', 64 | 'description' => 'An embedded HAL resource', 65 | 'type' => 'object', 66 | 'properties' => $items, 67 | ]; 68 | } 69 | 70 | $normalized['definitions']['linkArray'] = [ 71 | 'title' => 'HAL Link Array', 72 | 'description' => 'An array of linkObjects of the same link relation', 73 | 'type' => 'array', 74 | 'items' => [ 75 | '$ref' => '#/definitions/linkObject', 76 | ], 77 | ]; 78 | 79 | // Drupal core does not currently use several HAL link attributes. 80 | // If they are added entries should be added here. 81 | $normalized['definitions']['linkObject'] = [ 82 | 'title' => 'HAL Link Object', 83 | 'description' => 'An object with link information.', 84 | 'type' => 'object', 85 | 'properties' => [ 86 | 'name' => [ 87 | 'title' => 'Name', 88 | 'description' => 'Name of a resource, link, action, etc.', 89 | 'type' => 'string', 90 | ], 91 | 'title' => [ 92 | 'title' => 'Title', 93 | 'description' => 'A title for a resource, link, action, etc.', 94 | 'type' => 'string', 95 | ], 96 | 'href' => [ 97 | 'title' => 'HREF', 98 | 'description' => 'A hyperlink URL.', 99 | 'type' => 'string', 100 | 'format' => 'uri', 101 | ], 102 | ], 103 | 'required' => [ 104 | 'href', 105 | ], 106 | ]; 107 | 108 | return $normalized; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/json/ComplexDataDefinitionNormalizer.php: -------------------------------------------------------------------------------- 1 | extractPropertyData($entity); 33 | $normalized['type'] = 'object'; 34 | 35 | // Retrieve 'properties' and possibly 'required' nested arrays. 36 | $properties = $this->normalizeProperties( 37 | $entity->getPropertyDefinitions(), 38 | $format, 39 | $context 40 | ); 41 | 42 | $normalized = NestedArray::mergeDeep($normalized, $properties); 43 | return $normalized; 44 | } 45 | 46 | /** 47 | * Determine if the current field is a reference field. 48 | * 49 | * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface $entity 50 | * The complex data definition to be checked. 51 | * @param array $context 52 | * The current serializer context. 53 | * 54 | * @return bool 55 | * TRUE if it is a reference, FALSE otherwise. 56 | */ 57 | protected function isReferenceField(ComplexDataDefinitionInterface $entity, array $context = NULL) { 58 | $main = $entity->getPropertyDefinition($entity->getMainPropertyName()); 59 | // @todo use an interface or API call instead of an object check. 60 | return ($main instanceof DataReferenceTargetDefinition); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/json/DataDefinitionNormalizer.php: -------------------------------------------------------------------------------- 1 | getSetting('text source') 41 | || $entity->getSetting('date source')) { 42 | 43 | return []; 44 | } 45 | 46 | $property = $this->extractPropertyData($entity, $context); 47 | if (!empty($context['parent']) && $context['name'] == 'value') { 48 | if ($maxLength = $context['parent']->getSetting('max_length')) { 49 | $property['maxLength'] = $maxLength; 50 | } 51 | 52 | if (empty($context['parent']->getSetting('allowed_values_function')) 53 | && !empty($context['parent']->getSetting('allowed_values')) 54 | ) { 55 | $allowed_values = $context['parent']->getSetting('allowed_values'); 56 | $property['enum'] = array_keys($allowed_values); 57 | } 58 | } 59 | 60 | $normalized = ['properties' => []]; 61 | $normalized['properties'][$context['name']] = $property; 62 | if ($this->requiredProperty($entity)) { 63 | $normalized['required'][] = $context['name']; 64 | } 65 | 66 | return $normalized; 67 | } 68 | 69 | /** 70 | * Extracts property details from a data definition. 71 | * 72 | * This method includes mapping primitive types in Drupal to JSON Schema 73 | * type and format descriptions. This method is invoked by several of the 74 | * normalizers. 75 | * 76 | * @param \Drupal\Core\TypedData\DataDefinitionInterface $property 77 | * The data definition from which to extract values. 78 | * @param array $context 79 | * Serializer context. 80 | * 81 | * @return array 82 | * Discrete values of the property definition 83 | */ 84 | protected function extractPropertyData(DataDefinitionInterface $property, array $context = []) { 85 | return \Drupal::service('plugin.manager.schemata_json_schema.type_mapper') 86 | ->createInstance($property->getDataType()) 87 | ->getMappedValue($property); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/json/DataReferenceDefinitionNormalizer.php: -------------------------------------------------------------------------------- 1 | entityTypeManager = $entity_type_manager; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function normalize($entity, $format = NULL, array $context = []) { 46 | /* @var $entity \Drupal\Core\TypedData\DataReferenceDefinitionInterface */ 47 | if (!$this->validateEntity($entity)) { 48 | return []; 49 | } 50 | 51 | // DataDefinitionNormalizer::normalize() results in extraneous structures 52 | // added to the schema for this field element (e.g., entity) 53 | return $this->extractPropertyData($entity, $context); 54 | } 55 | 56 | /** 57 | * Ensure the entity type is one we support for schema reference. 58 | * 59 | * If somehow the entity does not exist, or is not a ContentEntity, skip it. 60 | * 61 | * @param mixed $entity 62 | * The object to be normalized. 63 | * 64 | * @return bool 65 | * TRUE if valid for use. 66 | */ 67 | protected function validateEntity($entity) { 68 | // Only entity references have a schema. 69 | // This leads to incompatibility with alternate reference modules such as 70 | // Dynamic Entity Reference. 71 | if ($entity->getDataType() != 'entity_reference') { 72 | return FALSE; 73 | } 74 | 75 | $entity_type_plugin = $this->entityTypeManager->getDefinition($entity->getConstraint('EntityType'), FALSE); 76 | if (empty($entity_type_plugin) 77 | || !($entity_type_plugin->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface'))) { 78 | return FALSE; 79 | } 80 | 81 | return TRUE; 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/json/FieldDefinitionNormalizer.php: -------------------------------------------------------------------------------- 1 | getDefaultValueLiteral(); 32 | if (!empty($default_value)) { 33 | $normalized['properties'][$context['name']]['default'] = $default_value; 34 | } 35 | 36 | // The cardinality is the configured maximum number of values the field can 37 | // contain. If unlimited, we do not include a maxItems attribute. 38 | $cardinality = $entity->getFieldStorageDefinition()->getCardinality(); 39 | if ($cardinality != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { 40 | $normalized['properties'][$context['name']]['maxItems'] = $cardinality; 41 | } 42 | 43 | return $normalized; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/json/JsonNormalizerBase.php: -------------------------------------------------------------------------------- 1 | extractPropertyData($entity, $context); 32 | $property['type'] = 'array'; 33 | 34 | // This retrieves the definition common to ever item in the list, and 35 | // serializes it so we can define how members of the array should look. 36 | // There are no lists that might contain items of different types. 37 | $property['items'] = $this->serializer->normalize( 38 | $entity->getItemDefinition(), 39 | $format, 40 | $context 41 | ); 42 | 43 | // FieldDefinitionInterface::isRequired() explicitly indicates there must be 44 | // at least one item in the list. Extending this reasoning, the same must be 45 | // true of all ListDataDefinitions. 46 | if ($this->requiredProperty($entity)) { 47 | $property['minItems'] = 1; 48 | } 49 | 50 | $normalized = ['properties' => []]; 51 | $normalized['properties'][$context['name']] = $property; 52 | if ($this->requiredProperty($entity)) { 53 | $normalized['required'][] = $context['name']; 54 | } 55 | 56 | return $normalized; 57 | } 58 | 59 | /** 60 | * Determine if the current field is a reference field. 61 | * 62 | * @param \Drupal\Core\TypedData\ListDataDefinitionInterface $entity 63 | * The list definition to be checked. 64 | * 65 | * @return bool 66 | * TRUE if it is a reference, FALSE otherwise. 67 | */ 68 | protected function isReferenceField(ListDataDefinitionInterface $entity) { 69 | $item = $entity->getItemDefinition(); 70 | if ($item instanceof ComplexDataDefinitionInterface) { 71 | $main = $item->getPropertyDefinition($item->getMainPropertyName()); 72 | // @todo use an interface or API call instead of an object check. 73 | return ($main instanceof DataReferenceTargetDefinition); 74 | } 75 | 76 | return FALSE; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/json/SchemataSchemaNormalizer.php: -------------------------------------------------------------------------------- 1 | format, $this->describedFormat, $entity) 27 | ->toString(TRUE); 28 | // Create the array of normalized fields, starting with the URI. 29 | $normalized = [ 30 | '$schema' => 'http://json-schema.org/draft-04/schema#', 31 | 'id' => $generated_url->getGeneratedUrl(), 32 | 'type' => 'object', 33 | ]; 34 | $normalized = array_merge($normalized, $entity->getMetadata()); 35 | 36 | // Stash schema request parameters. 37 | $context['entityTypeId'] = $entity->getEntityTypeId(); 38 | $context['bundleId'] = $entity->getBundleId(); 39 | 40 | // Retrieve 'properties' and possibly 'required' nested arrays. 41 | $properties = $this->normalizeProperties( 42 | $this->getProperties($entity, $format, $context), 43 | $format, 44 | $context 45 | ); 46 | $normalized = NestedArray::mergeDeep($normalized, $properties); 47 | 48 | return $normalized; 49 | } 50 | 51 | /** 52 | * Identify properties of the data definition to normalize. 53 | * 54 | * This allow subclasses of the normalizer to build white or blacklisting 55 | * functionality on what will be included in the serialized schema. The JSON 56 | * Schema serializer already has logic to drop any properties that are empty 57 | * values after processing, but this allows cleaner, centralized logic. 58 | * 59 | * @param \Drupal\schemata\Schema\SchemaInterface $entity 60 | * The Schema object whose properties the serializer will present. 61 | * @param string $format 62 | * The serializer format. Defaults to NULL. 63 | * @param array $context 64 | * The current serializer context. 65 | * 66 | * @return \Drupal\Core\TypedData\DataDefinitionInterface[] 67 | * The DataDefinitions to be processed. 68 | */ 69 | protected static function getProperties(SchemaInterface $entity, $format = NULL, array $context = []) { 70 | return $entity->getProperties(); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/jsonapi/ComplexDataDefinitionNormalizer.php: -------------------------------------------------------------------------------- 1 | extractPropertyData($entity); 33 | $normalized['type'] = 'object'; 34 | 35 | // Retrieve 'properties' and possibly 'required' nested arrays. 36 | $property_definitions = $entity->getPropertyDefinitions(); 37 | $properties = $this->normalizeProperties( 38 | $property_definitions, 39 | $format, 40 | $context 41 | ); 42 | 43 | $normalized = NestedArray::mergeDeep($normalized, $properties); 44 | if (count($property_definitions) == 1) { 45 | // If there is only one property, JSON API does not use the complex data. 46 | return $normalized['properties'][key($property_definitions)]; 47 | } 48 | return $normalized; 49 | } 50 | 51 | /** 52 | * Determine if the current field is a reference field. 53 | * 54 | * @param \Drupal\Core\TypedData\ComplexDataDefinitionInterface $entity 55 | * The complex data definition to be checked. 56 | * @param array $context 57 | * The current serializer context. 58 | * 59 | * @return bool 60 | * TRUE if it is a reference, FALSE otherwise. 61 | */ 62 | protected function isReferenceField(ComplexDataDefinitionInterface $entity, array $context = NULL) { 63 | $main = $entity->getPropertyDefinition($entity->getMainPropertyName()); 64 | // @todo use an interface or API call instead of an object check. 65 | return ($main instanceof DataReferenceTargetDefinition); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/jsonapi/DataDefinitionNormalizer.php: -------------------------------------------------------------------------------- 1 | getFieldStorageDefinition()->getCardinality(); 28 | $context['cardinality'] = $cardinality; 29 | /* @var $entity \Drupal\Core\Field\FieldDefinitionInterface */ 30 | $normalized = parent::normalize($entity, $format, $context); 31 | 32 | // Specify non-contextual default value as an example. 33 | $default_value = $entity->getDefaultValueLiteral(); 34 | if (!empty($default_value)) { 35 | $default_value = $cardinality == 1 ? reset($default_value) : $default_value; 36 | $default_value = count($default_value) == 1 ? reset($default_value) : $default_value; 37 | $normalized['properties']['attributes']['properties'][$context['name']]['default'] = $default_value; 38 | } 39 | 40 | // The cardinality is the configured maximum number of values the field can 41 | // contain. If unlimited, we do not include a maxItems attribute. 42 | if ($cardinality != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && $cardinality != 1) { 43 | $normalized['properties']['attributes']['properties'][$context['name']]['maxItems'] = $cardinality; 44 | } 45 | 46 | return $normalized; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/jsonapi/JsonApiNormalizerBase.php: -------------------------------------------------------------------------------- 1 | extractPropertyData($entity, $context); 32 | $property['type'] = 'array'; 33 | 34 | // This retrieves the definition common to ever item in the list, and 35 | // serializes it so we can define how members of the array should look. 36 | // There are no lists that might contain items of different types. 37 | $property['items'] = $this->serializer->normalize( 38 | $entity->getItemDefinition(), 39 | $format, 40 | $context 41 | ); 42 | 43 | // FieldDefinitionInterface::isRequired() explicitly indicates there must be 44 | // at least one item in the list. Extending this reasoning, the same must be 45 | // true of all ListDataDefinitions. 46 | if ($this->requiredProperty($entity)) { 47 | $property['minItems'] = 1; 48 | } 49 | 50 | if ($context['cardinality'] == 1) { 51 | $single_property = $property['items']; 52 | unset($property['items']); 53 | unset($property['type']); 54 | unset($property['minItems']); 55 | $single_property = array_merge($single_property, $property); 56 | $property = $single_property; 57 | } 58 | 59 | $normalized = [ 60 | 'description' => t('Entity attributes'), 61 | 'type' => 'object', 62 | 'properties' => [], 63 | ]; 64 | $normalized['properties'][$context['name']] = $property; 65 | if ($this->requiredProperty($entity)) { 66 | $normalized['required'][] = $context['name']; 67 | } 68 | 69 | return [ 70 | 'type' => 'object', 71 | 'properties' => ['attributes' => $normalized], 72 | ]; 73 | } 74 | 75 | /** 76 | * Determine if the current field is a reference field. 77 | * 78 | * @param \Drupal\Core\TypedData\ListDataDefinitionInterface $entity 79 | * The list definition to be checked. 80 | * 81 | * @return bool 82 | * TRUE if it is a reference, FALSE otherwise. 83 | */ 84 | protected function isReferenceField(ListDataDefinitionInterface $entity) { 85 | $item = $entity->getItemDefinition(); 86 | if ($item instanceof ComplexDataDefinitionInterface) { 87 | $main = $item->getPropertyDefinition($item->getMainPropertyName()); 88 | // @todo use an interface or API call instead of an object check. 89 | return ($main instanceof DataReferenceTargetDefinition); 90 | } 91 | 92 | return FALSE; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/jsonapi/RelationshipFieldDefinitionNormalizer.php: -------------------------------------------------------------------------------- 1 | fieldTypeManager = $field_type_manager; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function supportsNormalization($data, $format = NULL) { 47 | if (!parent::supportsNormalization($data, $format)) { 48 | return FALSE; 49 | } 50 | $type = $data->getItemDefinition()->getFieldDefinition()->getType(); 51 | $class = $this->fieldTypeManager->getPluginClass($type); 52 | // Deal only with entity reference fields and descendants. 53 | return $class == EntityReferenceItem::class || is_subclass_of($class, EntityReferenceItem::class); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function normalize($entity, $format = NULL, array $context = []) { 60 | $cardinality = $entity->getFieldStorageDefinition()->getCardinality(); 61 | $context['cardinality'] = $cardinality; 62 | /* @var $entity \Drupal\Core\Field\FieldDefinitionInterface */ 63 | $normalized = [ 64 | 'properties' => [ 65 | 'relationships' => [ 66 | 'description' => t('Entity relationships'), 67 | 'properties' => [$context['name'] => $this->normalizeRelationship($entity)], 68 | 'type' => 'object', 69 | ], 70 | ], 71 | ]; 72 | // Specify non-contextual default value as an example. 73 | $default_value = $entity->getDefaultValueLiteral(); 74 | if (!empty($default_value)) { 75 | $normalized['properties']['relationships']['properties'][$context['name']]['default'] = $default_value; 76 | } 77 | 78 | // The cardinality is the configured maximum number of values the field can 79 | // contain. If unlimited, we do not include a maxItems attribute. 80 | if ($cardinality != FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && $cardinality != 1) { 81 | $normalized['properties']['relationships']['properties'][$context['name']]['maxItems'] = $cardinality; 82 | } 83 | 84 | return $normalized; 85 | } 86 | 87 | /** 88 | * Normalizes the relationship. 89 | * 90 | * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition 91 | * The field definition. 92 | * 93 | * @return array 94 | * The normalized relationship. 95 | */ 96 | protected function normalizeRelationship(FieldDefinitionInterface $field_definition) { 97 | // A relationship has very similar schema every time. 98 | $resource_identifier_object = [ 99 | 'type' => 'object', 100 | 'required' => ['type', 'id'], 101 | 'properties' => [ 102 | 'type' => ['type' => 'string', 'title' => t('Referenced resource')], 103 | 'id' => [ 104 | 'type' => 'string', 105 | 'title' => t('Resource ID'), 106 | 'format' => 'uuid', 107 | 'maxLength' => 128, 108 | ], 109 | ], 110 | ]; 111 | // Handle the multivalue variant. 112 | $cardinality = $field_definition 113 | ->getFieldStorageDefinition() 114 | ->getCardinality(); 115 | /* @var $entity \Drupal\Core\TypedData\DataReferenceDefinitionInterface */ 116 | if ($target_entity_type = $field_definition->getSetting('target_type')) { 117 | $handler_settings = $field_definition->getSetting('handler_settings'); 118 | $target_bundles = empty($handler_settings['target_bundles']) ? 119 | [$target_entity_type] : 120 | array_values($handler_settings['target_bundles']); 121 | $enum = array_map(function ($bundle) use ($target_entity_type) { 122 | return sprintf('%s--%s', $target_entity_type, $bundle); 123 | }, $target_bundles); 124 | } 125 | if ($cardinality == 1) { 126 | $data = $resource_identifier_object; 127 | if (!empty($enum)) { 128 | $data['properties']['type']['enum'] = $enum; 129 | } 130 | } 131 | else { 132 | $data = [ 133 | 'type' => 'array', 134 | 'items' => $resource_identifier_object, 135 | ]; 136 | if (!empty($enum)) { 137 | $data['items']['properties']['type']['enum'] = $enum; 138 | } 139 | } 140 | $normalized = [ 141 | 'type' => 'object', 142 | 'properties' => [ 143 | 'data' => $data, 144 | ], 145 | 'title' => t('Resource Identifier'), 146 | ]; 147 | 148 | return $normalized; 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Normalizer/jsonapi/SchemataSchemaNormalizer.php: -------------------------------------------------------------------------------- 1 | format, $this->describedFormat, $entity) 27 | ->toString(TRUE); 28 | // Create the array of normalized fields, starting with the URI. 29 | $normalized = [ 30 | '$schema' => 'http://json-schema.org/draft-04/schema#', 31 | 'id' => $generated_url->getGeneratedUrl(), 32 | 'type' => 'object', 33 | 'properties' => [ 34 | 'type' => [ 35 | 'type' => 'string', 36 | 'title' => 'type', 37 | 'description' => t('Resource type'), 38 | ], 39 | 'id' => [ 40 | 'type' => 'string', 41 | 'title' => t('Resource ID'), 42 | 'format' => 'uuid', 43 | 'maxLength' => 128, 44 | ], 45 | ], 46 | ]; 47 | 48 | // Stash schema request parameters. 49 | $context['entityTypeId'] = $entity->getEntityTypeId(); 50 | $context['bundleId'] = $entity->getBundleId(); 51 | 52 | // Retrieve 'properties' and possibly 'required' nested arrays. 53 | $properties = $this->normalizeProperties( 54 | $this->getProperties($entity, $format, $context), 55 | $format, 56 | $context 57 | ); 58 | $links = [ 59 | 'properties' => [ 60 | 'links' => [ 61 | 'type' => 'object', 62 | 'description' => t('Entity links'), 63 | 'properties' => [ 64 | 'self' => [ 65 | 'type' => 'string', 66 | 'format' => 'uri', 67 | 'description' => t('The absolute link to this entity.'), 68 | ], 69 | ], 70 | ], 71 | ], 72 | ]; 73 | return NestedArray::mergeDeep($normalized, $entity->getMetadata(), $properties, $links); 74 | } 75 | 76 | /** 77 | * Identify properties of the data definition to normalize. 78 | * 79 | * This allow subclasses of the normalizer to build white or blacklisting 80 | * functionality on what will be included in the serialized schema. The JSON 81 | * Schema serializer already has logic to drop any properties that are empty 82 | * values after processing, but this allows cleaner, centralized logic. 83 | * 84 | * @param \Drupal\schemata\Schema\SchemaInterface $entity 85 | * The Schema object whose properties the serializer will present. 86 | * @param string $format 87 | * The serializer format. Defaults to NULL. 88 | * @param array $context 89 | * The current serializer context. 90 | * 91 | * @return \Drupal\Core\TypedData\DataDefinitionInterface[] 92 | * The DataDefinitions to be processed. 93 | */ 94 | protected static function getProperties(SchemaInterface $entity, $format = NULL, array $context = []) { 95 | return $entity->getProperties(); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Plugin/Type/TypeMapperPluginManager.php: -------------------------------------------------------------------------------- 1 | getDataType(); 22 | return $value; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Plugin/schemata_json_schema/type_mapper/FilterFormatTypeMapper.php: -------------------------------------------------------------------------------- 1 | $this->getPluginId(), 41 | ]; 42 | 43 | if ($item = $property->getLabel()) { 44 | $value['title'] = $item; 45 | } 46 | if ($item = $property->getDescription()) { 47 | $value['description'] = addslashes(strip_tags($item)); 48 | } 49 | 50 | return $value; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /schemata_json_schema/src/Plugin/schemata_json_schema/type_mapper/TypeMapperInterface.php: -------------------------------------------------------------------------------- 1 | getParameter('container.modules'); 19 | if (!isset($modules['hal'])) { 20 | return; 21 | } 22 | 23 | // Provide the HAL+JSON version of the Data Reference normalizer here 24 | // because the hal.link_manager service argument requires HAL. 25 | $container->register('serializer.normalizer.data_reference_definition.schema_json.hal_json', 'Drupal\schemata_json_schema\Normalizer\hal\DataReferenceDefinitionNormalizer') 26 | ->addArgument(new Reference('entity_type.manager')) 27 | ->addArgument(new Reference('hal.link_manager')) 28 | ->addTag('normalizer', ['priority' => 30]); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/Controller/Controller.php: -------------------------------------------------------------------------------- 1 | serializer = $serializer; 52 | $this->schemaFactory = $schema_factory; 53 | $this->response = $response; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public static function create(ContainerInterface $container) { 60 | return new static( 61 | $container->get('serializer'), 62 | $container->get('schemata.schema_factory'), 63 | new CacheableResponse() 64 | ); 65 | } 66 | 67 | /** 68 | * Serializes a entity type or bundle definition. 69 | * 70 | * We have 2 different data formats involved. One is the schema format (for 71 | * instance JSON Schema) and the other one is the format that the schema is 72 | * describing (for instance jsonapi, json, hal+json, …). We need to provide 73 | * both formats. Something like: ?_format=schema_json&_describes=api_json. 74 | * 75 | * @param string $entity_type_id 76 | * The entity type ID to describe. 77 | * @param string $bundle 78 | * The (optional) bundle to describe. 79 | * @param \Symfony\Component\HttpFoundation\Request $request 80 | * The request object. 81 | * 82 | * @return \Drupal\Core\Cache\CacheableResponse 83 | * The response object. 84 | */ 85 | public function serialize($entity_type_id, Request $request, $bundle = NULL) { 86 | $parts = $this->extractFormatNames($request); 87 | 88 | // Load the data to serialize from the route information on the current 89 | // request. 90 | $schema = $this->schemaFactory->create($entity_type_id, $bundle); 91 | // Serialize the entity type/bundle definition. 92 | $format = implode(':', $parts); 93 | $content = $this->serializer->serialize($schema, $format); 94 | 95 | // Finally, set the contents of the response and return it. 96 | $this->response->addCacheableDependency($schema); 97 | $cacheable_dependency = (new CacheableMetadata()) 98 | ->addCacheContexts(['url.query_args:_describes']); 99 | $this->response->addCacheableDependency($cacheable_dependency); 100 | $this->response->setContent($content); 101 | $this->response->headers->set('Content-Type', $request->getMimeType($parts[0])); 102 | return $this->response; 103 | } 104 | 105 | /** 106 | * Helper function that inspects the request to extract the formats. 107 | * 108 | * Extracts the format of the response and media type being described. 109 | * 110 | * @param \Symfony\Component\HttpFoundation\Request $request 111 | * The request object. 112 | * 113 | * @return array 114 | * An array containing the format of the output and the media type being 115 | * described. 116 | */ 117 | protected function extractFormatNames(Request $request) { 118 | return [ 119 | $request->getRequestFormat(), 120 | $request->query->get('_describes', ''), 121 | ]; 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/Encoder/JsonSchemaEncoder.php: -------------------------------------------------------------------------------- 1 | format; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function supportsDecoding($format) { 33 | return FALSE; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Normalizer/NormalizerBase.php: -------------------------------------------------------------------------------- 1 | $property) { 52 | $context['name'] = $name; 53 | $item = $this->serializer->normalize($property, $format, $context); 54 | if (!empty($item)) { 55 | $normalized = NestedArray::mergeDeep($normalized, $item); 56 | } 57 | } 58 | 59 | return $normalized; 60 | } 61 | 62 | /** 63 | * Determine if the given property is a required element of the schema. 64 | * 65 | * @param \Drupal\Core\TypedData\DataDefinitionInterface $property 66 | * The data property to be evaluated. 67 | * 68 | * @return bool 69 | * Whether the property should be treated as required for schema 70 | * purposes. 71 | */ 72 | protected function requiredProperty(DataDefinitionInterface $property) { 73 | return $property->isReadOnly() || $property->isRequired(); 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | protected function checkFormat($format = NULL) { 80 | if (!isset($format) || !isset($this->format)) { 81 | return TRUE; 82 | } 83 | 84 | $parts = explode(':', $format); 85 | return $parts[0] == $this->format && isset($parts[1]) && $this->describedFormat == $parts[1]; 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/Routing/Routes.php: -------------------------------------------------------------------------------- 1 | entityTypeManager = $entity_type_manager; 53 | $this->entityTypeBundleInfo = $entity_type_bundle_info; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public static function create(ContainerInterface $container) { 60 | return new static( 61 | $container->get('entity_type.manager'), 62 | $container->get('entity_type.bundle.info') 63 | ); 64 | } 65 | 66 | /** 67 | * The route generator. 68 | */ 69 | public function routes() { 70 | $route_collection = new RouteCollection(); 71 | // Loop through all the entity types. 72 | foreach ($this->entityTypeManager->getDefinitions() as $entity_type) { 73 | $entity_type_id = $entity_type->id(); 74 | // Add a route for all entity types. 75 | $route_collection->add($this->createRouteName($entity_type_id), $this->createRoute($entity_type_id)); 76 | 77 | // If this entity type has a bundle entity type, 78 | // then add a route for each bundle. 79 | if ($entity_type->getBundleEntityType()) { 80 | // Loop through all the bundles for the entity type. 81 | $bundles_info = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id); 82 | foreach (array_keys($bundles_info) as $bundle) { 83 | $route_collection->add($this->createRouteName($entity_type_id, $bundle), $this->createRoute($entity_type_id, $bundle)); 84 | } 85 | } 86 | } 87 | return $route_collection; 88 | } 89 | 90 | /** 91 | * Creates a route for a entity type and bundle. 92 | * 93 | * @param string $entity_type_id 94 | * The entity type id. 95 | * @param string $bundle 96 | * The bundle name. 97 | * 98 | * @return \Symfony\Component\Routing\Route 99 | * The route. 100 | */ 101 | protected function createRoute($entity_type_id, $bundle = NULL) { 102 | $path = $this->getRoutePath($entity_type_id, $bundle); 103 | $route = new Route($path); 104 | $route->setRequirement('_permission', 'access schemata data models'); 105 | $route->setMethods(['GET']); 106 | $defaults = [ 107 | 'entity_type_id' => $entity_type_id, 108 | RouteObjectInterface::CONTROLLER_NAME => static::CONTROLLER, 109 | ]; 110 | if ($bundle) { 111 | $defaults['bundle'] = $bundle; 112 | } 113 | $route->setDefaults($defaults); 114 | return $route; 115 | } 116 | 117 | /** 118 | * Creates a route name for a entity type and bundle. 119 | * 120 | * @param string $entity_type_id 121 | * The entity type id. 122 | * @param string $bundle 123 | * The bundle name. 124 | * 125 | * @return string 126 | * The route name. 127 | */ 128 | protected function createRouteName($entity_type_id, $bundle = NULL) { 129 | return $bundle ? sprintf('schemata.%s:%s', $entity_type_id, $bundle) : sprintf('schemata.%s', $entity_type_id); 130 | } 131 | 132 | /** 133 | * Creates a route path for a entity type and bundle. 134 | * 135 | * @param string $entity_type_id 136 | * The entity type id. 137 | * @param string $bundle 138 | * The bundle name. 139 | * 140 | * @return string 141 | * The route path. 142 | */ 143 | protected function getRoutePath($entity_type_id, $bundle = NULL) { 144 | $path = $bundle ? 145 | sprintf('/schemata/%s/%s', $entity_type_id, $bundle) : 146 | sprintf('/schemata/%s', $entity_type_id); 147 | return $path; 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /src/Schema/NodeSchema.php: -------------------------------------------------------------------------------- 1 | nodeType = NodeType::load($bundle); 27 | parent::__construct($entity_type, $bundle, $properties); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function createDescription($entityType, $bundle = '') { 34 | $description = $this->nodeType->getDescription(); 35 | if (empty($description)) { 36 | return parent::createDescription($entityType, $bundle); 37 | } 38 | 39 | return addslashes(strip_tags($description));; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/Schema/Schema.php: -------------------------------------------------------------------------------- 1 | entityType = $entity_type; 70 | $this->bundle = $bundle; 71 | $this->addProperties($properties); 72 | 73 | // These cache tags seem like they are essential to Schema cache variation. 74 | // It is not clear how to verify these cache tags, or how to pull them from 75 | // the injected dependencies. 76 | $this->addCacheableDependency((new CacheableMetadata())->addCacheTags( 77 | [ 78 | 'entity_bundles', 79 | 'entity_field_info', 80 | 'entity_types', 81 | ] 82 | )); 83 | 84 | $this->metadata['title'] = $this->createTitle( 85 | $this->getEntityTypeId(), 86 | $this->getBundleId() 87 | ) . ' Schema'; 88 | 89 | $this->metadata['description'] = $this->createDescription( 90 | $this->getEntityTypeId(), 91 | $this->getBundleId() 92 | ); 93 | 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function addProperties(array $properties) { 100 | $this->properties += $properties; 101 | } 102 | 103 | /** 104 | * {@inheritdoc} 105 | */ 106 | public function getEntityTypeId() { 107 | return $this->entityType->getEntityTypeId(); 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | public function getBundleId() { 114 | return $this->bundle; 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | */ 120 | public function getProperties() { 121 | return $this->properties; 122 | } 123 | 124 | /** 125 | * {@inheritdoc} 126 | */ 127 | public function getMetadata() { 128 | return $this->metadata; 129 | } 130 | 131 | /** 132 | * Generates a title for the schema based on a simple trio of parameters. 133 | * 134 | * @param string $required 135 | * This value will be used as the prefix. 136 | * @param string $optional 137 | * Optional value to be separated from the required prefix if present. 138 | * @param string $sep 139 | * Separator string to be used. 140 | * 141 | * @return string 142 | * The title value. 143 | */ 144 | protected function createTitle($required, $optional = '', $sep = ':') { 145 | return empty($optional) ? $required : $required . $sep . $optional; 146 | } 147 | 148 | /** 149 | * Generates a description for the schema based on the types. 150 | * 151 | * @param string $entityType 152 | * Required entity type which the schema defines. 153 | * @param string $bundle 154 | * Optional value to be separated from the required prefix if present. 155 | * 156 | * @return string 157 | * The description value. 158 | */ 159 | protected function createDescription($entityType, $bundle = '') { 160 | $output = "Describes the payload for '$entityType' entities"; 161 | if (!empty($bundle)) { 162 | $output .= " of the '$bundle' bundle."; 163 | } 164 | else { 165 | $output .= '.'; 166 | } 167 | 168 | return $output; 169 | } 170 | 171 | /** 172 | * Magic method: Unsets a property. 173 | * 174 | * @param string $name 175 | * The name of the property to unset; e.g., 'title' or 'name'. 176 | */ 177 | public function __unset($name) { 178 | unset($this->properties[$name]); 179 | } 180 | 181 | /** 182 | * Magic method: Determines whether a property is set. 183 | * 184 | * @param string $name 185 | * The name of the property to check; e.g., 'title' or 'name'. 186 | * 187 | * @return bool 188 | * Returns TRUE if the property exists and is set, FALSE otherwise. 189 | */ 190 | public function __isset($name) { 191 | return isset($this->properties[$name]); 192 | } 193 | 194 | /** 195 | * Magic method: Sets a property value. 196 | * 197 | * @param string $name 198 | * The name of the property to set; e.g., 'title' or 'name'. 199 | * @param \Drupal\Core\TypedData\DataDefinitionInterface $value 200 | * The value to set. 201 | */ 202 | public function __set($name, DataDefinitionInterface $value) { 203 | $this->properties[$name] = $value; 204 | } 205 | 206 | /** 207 | * Magic method: Gets a property value. 208 | * 209 | * @param string $name 210 | * The name of the property to get; e.g., 'title' or 'name'. 211 | */ 212 | public function __get($name) { 213 | $this->properties[$name] = $value; 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /src/Schema/SchemaInterface.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 61 | $this->entityTypeManager = $entity_type_manager; 62 | $this->entityTypeBundleInfo = $entity_type_bundle_info; 63 | $this->typedDataManager = $typed_data_manager; 64 | } 65 | 66 | /** 67 | * Assemble a schema object based on the requested entity type. 68 | * 69 | * @param string $entity_type 70 | * URL input specifying an entity type to be processed. 71 | * @param string $bundle 72 | * URL input specifying an entity bundle to be processed. May be NULL 73 | * for support of entities that do not have bundles. 74 | * 75 | * @return \Drupal\schemata\Schema\SchemaInterface 76 | * A Schema object which can be processed as a Rest Resource response. 77 | */ 78 | public function create($entity_type, $bundle = NULL) { 79 | try { 80 | $entity_type_plugin = $this->getSourceEntityPlugin($entity_type); 81 | } 82 | catch(\Exception $e) { 83 | $this->logger->error($e->getMessage()); 84 | // @todo Handle these exceptions in https://www.drupal.org/node/2868562. 85 | return NULL; 86 | } 87 | 88 | $entity_type_plugin = $this->entityTypeManager->getDefinition($entity_type, FALSE); 89 | if ($entity_type_plugin->getBundleEntityType()) { 90 | $bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type); 91 | } 92 | if (!empty($bundle) && !array_key_exists($bundle, $bundles)) { 93 | $this->logger->warning('Specified Entity Bundle "%bundle" does not exist.', [ 94 | '%bundle' => $bundle, 95 | ]); 96 | return NULL; 97 | } 98 | 99 | $data_definition = empty($bundle) ? 100 | $this->typedDataManager->createDataDefinition("entity:" . $entity_type) : 101 | $this->typedDataManager->createDataDefinition("entity:" . $entity_type . ":" . $bundle); 102 | 103 | if ($entity_type == 'node' && !empty($bundle)) { 104 | $class = '\Drupal\schemata\Schema\NodeSchema'; 105 | } 106 | else { 107 | $class = '\Drupal\schemata\Schema\Schema'; 108 | } 109 | 110 | $schema = new $class( 111 | $data_definition, 112 | $bundle, 113 | $data_definition->getPropertyDefinitions() 114 | ); 115 | 116 | $this->logger->notice('Schema generated for Entity Type (%entity_type) and Bundle (%bundle).', [ 117 | '%entity_type' => $entity_type, 118 | '%bundle' => empty($bundle) ? 'N/A' : $bundle, 119 | ]); 120 | 121 | return $schema; 122 | } 123 | 124 | /** 125 | * Load the Entity Type Plugin to drive schema content. 126 | * 127 | * This method should incorporate any "validation" of the entity type. 128 | * 129 | * It is broken out in part to facilitate test creation. 130 | * 131 | * @param string $entity_type_id 132 | * Entity Type ID. 133 | * 134 | * @return \Drupal\Entity\EntityTypeInterface 135 | * The Entity Type plugin. 136 | * 137 | * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException 138 | * Thrown if $entity_type_id does not refer to a valid Entity Type plugin. 139 | * 140 | * @throws \InvalidArgumentException 141 | * Thrown if $entity_type_id does not refer to a Content Entity Type. 142 | * 143 | * @see \Drupal\Tests\schemata\Functional\ValidateSchemaTest::validateSchemaAsJsonSchema() 144 | */ 145 | public function getSourceEntityPlugin($entity_type_id) { 146 | $entity_type_plugin = $this->entityTypeManager->getDefinition($entity_type_id); 147 | if (!($entity_type_plugin->isSubclassOf('\Drupal\Core\Entity\ContentEntityInterface'))) { 148 | throw new \InvalidArgumentException(sprintf('Entity Type %s is not a content entity. Only content entities are supported at this time.', $entity_type_id)); 149 | } 150 | 151 | return $entity_type_plugin; 152 | } 153 | 154 | 155 | 156 | } 157 | -------------------------------------------------------------------------------- /src/SchemaUrl.php: -------------------------------------------------------------------------------- 1 | getEntityTypeId(), 35 | $schema->getBundleId() 36 | ); 37 | } 38 | 39 | /** 40 | * Build a URI to a schema resource. 41 | * 42 | * @param string $format 43 | * The format or type of schema. 44 | * @param string $describes 45 | * The format being described. 46 | * @param string $entity_type_id 47 | * The entity type. 48 | * @param string $bundle 49 | * The entity bundle. 50 | * 51 | * @return \Drupal\Core\Url 52 | * The schema resource Url object. 53 | */ 54 | public static function fromOptions($format, $describes, $entity_type_id, $bundle = NULL) { 55 | $route_name = empty($bundle) 56 | ? sprintf('schemata.%s', $entity_type_id) 57 | : sprintf('schemata.%s:%s', $entity_type_id, $bundle); 58 | 59 | return Url::fromRoute($route_name, [], [ 60 | 'query' => [ 61 | '_format' => $format, 62 | '_describes' => $describes, 63 | ], 64 | 'absolute' => TRUE, 65 | ]); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/SchemataServiceProvider.php: -------------------------------------------------------------------------------- 1 | has('http_middleware.negotiation') && is_a($container->getDefinition('http_middleware.negotiation') 18 | ->getClass(), '\Drupal\Core\StackMiddleware\NegotiationMiddleware', TRUE) 19 | ) { 20 | // @see https://www.ietf.org/id/draft-wright-json-schema-00.txt 21 | $container->getDefinition('http_middleware.negotiation') 22 | ->addMethodCall('registerFormat', [ 23 | 'schema_json', 24 | ['application/schema+json'], 25 | ]); 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /tests/phpcs-ruleset.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Drupal coding standard for contributed modules 6 | 7 | 8 | *.gif 9 | *.less 10 | *.png 11 | 12 | 13 | *.min.css 14 | *.min.js 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Default PHP CodeSniffer configuration for Organic Groups. 4 | 5 | 6 | 7 | 8 | . 9 | ./vendor 10 | 11 | -------------------------------------------------------------------------------- /tests/src/Functional/SchemataBrowserTestBase.php: -------------------------------------------------------------------------------- 1 | entityTypeManager = $this->container->get('entity_type.manager'); 63 | $this->schemaFactory = $this->container->get('schemata.schema_factory'); 64 | 65 | if (!NodeType::load('camelids')) { 66 | // Create a "Camelids" node type. 67 | NodeType::create([ 68 | 'name' => 'Camelids', 69 | 'type' => 'camelids', 70 | ])->save(); 71 | } 72 | 73 | // Create a "Camelids" vocabulary. 74 | $vocabulary = Vocabulary::create([ 75 | 'name' => 'Camelids', 76 | 'vid' => 'camelids', 77 | ]); 78 | $vocabulary->save(); 79 | 80 | $entity_types = ['node', 'taxonomy_term']; 81 | foreach ($entity_types as $entity_type) { 82 | // Add access-protected field. 83 | FieldStorageConfig::create([ 84 | 'entity_type' => $entity_type, 85 | 'field_name' => 'field_test_' . $entity_type, 86 | 'type' => 'text', 87 | ]) 88 | ->setCardinality(1) 89 | ->save(); 90 | FieldConfig::create([ 91 | 'entity_type' => $entity_type, 92 | 'field_name' => 'field_test_' . $entity_type, 93 | 'bundle' => 'camelids', 94 | ]) 95 | ->setLabel('Test field') 96 | ->setTranslatable(FALSE) 97 | ->save(); 98 | } 99 | $this->container->get('router.builder')->rebuild(); 100 | $this->drupalLogin($this->drupalCreateUser(['access schemata data models'])); 101 | } 102 | 103 | /** 104 | * Retrieve the schema by URL and dereference for use. 105 | * 106 | * Dereferencing a schema processes references to external schema documents 107 | * and prepares it to be used as a validation authority. 108 | * 109 | * This will static cache the schema so the same schema resource will not be 110 | * retrieved and processed more than once per test run. 111 | * 112 | * @param string $url 113 | * Absolute URL to a JSON Schema resource. 114 | * 115 | * @return object 116 | * Dereferenced schema object. 117 | * 118 | * @todo Evaluate PSR-16 cache support built into Dereferencer. 119 | * 120 | * @see http://json-reference.thephpleague.com/caching 121 | */ 122 | protected function getDereferencedSchema($url) { 123 | if (empty($this->schemaCache[$url])) { 124 | $dereferencer = Dereferencer::draft4(); 125 | // By definition of the JSON Schema spec, schemas use this key to refer 126 | // to the schema to which they conform. 127 | $this->schemaCache[$url] = $dereferencer->dereference($url); 128 | } 129 | 130 | return $this->schemaCache[$url]; 131 | } 132 | 133 | /** 134 | * Requests a Schema via HTTP, ready for session assertions. 135 | * 136 | * @param string $format 137 | * The described format. 138 | * @param string $entity_type_id 139 | * Then entity type. 140 | * @param string|null $bundle_id 141 | * The bundle name or NULL. 142 | * 143 | * @return string 144 | * Serialized schema contents. 145 | */ 146 | protected function getRawSchemaByOptions($format, $entity_type_id, $bundle_id = NULL) { 147 | $url = SchemaUrl::fromOptions('schema_json', $format, $entity_type_id, $bundle_id)->toString(); 148 | return $this->drupalGet($url); 149 | } 150 | 151 | /** 152 | * Requests a dereferenced Schema via HTTP. 153 | * 154 | * Dereferencing a schema processes references to external schema documents 155 | * and prepares it to be used as a validation authority. 156 | * 157 | * @param string $format 158 | * The described format. 159 | * @param string $entity_type_id 160 | * Then entity type. 161 | * @param string|null $bundle_id 162 | * The bundle name or NULL. 163 | * 164 | * @return object 165 | * Dereferenced schema object. 166 | */ 167 | protected function getDereferencedSchemaByOptions($format, $entity_type_id, $bundle_id = NULL) { 168 | $url = SchemaUrl::fromOptions('schema_json', $format, $entity_type_id, $bundle_id)->toString(); 169 | return $this->requestSchemaByUrl($url); 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /tests/src/Functional/ValidateSchemaTest.php: -------------------------------------------------------------------------------- 1 | entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { 24 | try { 25 | // Retrieving the plugin is redundant, but if this succeeds without 26 | // throwing an error, it's a valid entity type for schema generation. 27 | $this->schemaFactory->getSourceEntityPlugin($entity_type_id); 28 | } 29 | catch (\Exception $e) { 30 | // Skip schema validation testing of invalid entity types. 31 | // @todo add test coverage of skipped Config Entity schema generation 32 | // in https://www.drupal.org/node/2868562 or related follow-up. 33 | continue; 34 | } 35 | $this->validateSchemaAsJsonSchema($described_format, $entity_type_id); 36 | if ($bundle_type = $entity_type->getBundleEntityType()) { 37 | $bundles = $this->entityTypeManager->getStorage($bundle_type)->loadMultiple(); 38 | foreach ($bundles as $bundle) { 39 | $this->validateSchemaAsJsonSchema($described_format, $entity_type_id, $bundle->id()); 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * Confirm a schema is inherently valid as a JSON Schema. 48 | * 49 | * @param string $format 50 | * The described format. 51 | * @param string $entity_type_id 52 | * Then entity type. 53 | * @param string|null $bundle_id 54 | * The bundle name or NULL. 55 | * 56 | * @todo how do you handle tests that should stop processing on a failure. 57 | */ 58 | protected function validateSchemaAsJsonSchema($format, $entity_type_id, $bundle_id = NULL) { 59 | $json = $this->getRawSchemaByOptions($format, $entity_type_id, $bundle_id); 60 | $this->assertSession()->statusCodeEquals(200); 61 | // Requesting config entity schema gets a 200 response but no content. 62 | // @todo return an error on requesting config schema. 63 | $this->assertTrue(!empty($json), 'Schema request should provide response content instead of a NULL value.'); 64 | 65 | try { 66 | $data = json_decode($json); 67 | } 68 | catch (Exception $e) { 69 | $this->assertTrue(FALSE, "Could not decode JSON from schema response. Error: " . $e->getMessage()); 70 | } 71 | $this->assertNotEmpty($data->{'$schema'}, 'JSON Schema should include a $schema reference to a defining schema.'); 72 | 73 | // Prepare the schema for validation. 74 | // By definition of the JSON Schema spec, schemas use a top-level '$schema' 75 | // key to identify the schema specification with which they conform. 76 | $schema = $this->getDereferencedSchema($data->{'$schema'}); 77 | $this->assertTrue(!empty($schema), 'The schema specification must be retrieved and dereferenced for use.'); 78 | 79 | // Validate our schema is a correct schema. 80 | $validator = new Validator($data, $schema); 81 | if ($validator->fails()) { 82 | $bundle_label = empty($bundle_id) ? 'no-bundle' : $bundle_id; 83 | $message = "Schema ($entity_type_id:$bundle_label) failed validation for $format:\n"; 84 | $errors = $validator->errors(); 85 | $message .= json_encode($errors, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 86 | $this->assertTrue(FALSE, $message); 87 | } 88 | 89 | // Now that the schema has validated correctly, let's confirm an invalid 90 | // schema will fail validation. 91 | $data->properties = ''; 92 | $validator = new Validator($data, $schema); 93 | if (!$validator->fails()) { 94 | $bundle_label = empty($bundle_id) ? 'no-bundle' : $bundle_id; 95 | $message = "Schema ($entity_type_id:$bundle_label) should fail validation if it is wrong.\n"; 96 | $this->assertTrue(FALSE, $message); 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /tests/src/Kernel/SchemaFactoryTest.php: -------------------------------------------------------------------------------- 1 | installEntitySchema('node'); 46 | $this->installEntitySchema('user'); 47 | // Add the additional table schemas. 48 | $this->installSchema('system', ['sequences']); 49 | $this->installSchema('node', ['node_access']); 50 | $this->installSchema('user', ['users_data']); 51 | 52 | $this->nodeType = NodeType::create([ 53 | 'type' => 'article', 54 | ]); 55 | $this->nodeType->save(); 56 | 57 | $this->factory = \Drupal::service('schemata.schema_factory'); 58 | } 59 | 60 | /** 61 | * @covers ::create 62 | */ 63 | public function testCreateNodeBaseSchema() { 64 | $schema = $this->factory->create('node'); 65 | $this->assertInstanceOf(SchemaInterface::class, $schema); 66 | $this->assertNotInstanceOf(NodeSchema::class, $schema); 67 | $this->assertSchemaHasNoBundle($schema); 68 | $this->assertSchemaHasTitle($schema); 69 | $this->assertSchemaHasDescription($schema); 70 | $this->assertSchemaHasProperties($schema); 71 | } 72 | 73 | /** 74 | * @covers ::create 75 | */ 76 | public function testCreateNodeArticleSchema() { 77 | $schema = $this->factory->create('node', 'article'); 78 | $this->assertInstanceOf(SchemaInterface::class, $schema); 79 | $this->assertInstanceOf(NodeSchema::class, $schema); 80 | $this->assertSchemaHasBundle($schema, 'article'); 81 | $this->assertSchemaHasTitle($schema); 82 | $this->assertSchemaHasDescription($schema); 83 | $this->assertSchemaHasProperties($schema); 84 | } 85 | 86 | /** 87 | * @covers ::create 88 | */ 89 | public function testCreateUserSchema() { 90 | $schema = $this->factory->create('user'); 91 | $this->assertInstanceOf(SchemaInterface::class, $schema); 92 | $this->assertNotInstanceOf(NodeSchema::class, $schema); 93 | $this->assertSchemaHasNoBundle($schema); 94 | $this->assertSchemaHasTitle($schema); 95 | $this->assertSchemaHasDescription($schema); 96 | $this->assertSchemaHasProperties($schema); 97 | } 98 | 99 | /** 100 | * @covers ::create 101 | */ 102 | public function testInvalidEntityOnCreate() { 103 | $schema = $this->factory->create('gastropod'); 104 | $this->assertEmpty($schema, 'Schemata should not produce a schema for non-existant entity types.'); 105 | $schema = $this->factory->create('node', 'gastropod'); 106 | $this->assertEmpty($schema, 'Schemata should not produce a schema for non-existant bundles.'); 107 | } 108 | 109 | /** 110 | * @covers ::getSourceEntityPlugin 111 | */ 112 | public function testInvalidEntityOnGetPlugin() { 113 | $this->setExpectedException('\Drupal\Component\Plugin\Exception\PluginNotFoundException'); 114 | $this->factory->getSourceEntityPlugin('gastropod'); 115 | } 116 | 117 | /** 118 | * @covers ::create 119 | */ 120 | public function testConfigEntityOnCreate() { 121 | $schema = $this->factory->create('node_type'); 122 | $this->assertEmpty($schema, 'Schemata does not support Config entities.'); 123 | } 124 | 125 | /** 126 | * @covers ::getSourceEntityPlugin 127 | */ 128 | public function testConfigEntityOnGetPlugin() { 129 | $this->setExpectedException('\InvalidArgumentException'); 130 | $this->factory->getSourceEntityPlugin('node_type'); 131 | } 132 | 133 | /** 134 | * Assert the schema has a title. 135 | * 136 | * @param \Drupal\schemata\Schema\SchemaInterface $schema 137 | * Schema to evaluate. 138 | */ 139 | protected function assertSchemaHasTitle(SchemaInterface $schema) { 140 | $this->assertNotEmpty($schema->getMetadata()['title']); 141 | } 142 | 143 | /** 144 | * Assert the schema has a description. 145 | * 146 | * @param \Drupal\schemata\Schema\SchemaInterface $schema 147 | * Schema to evaluate. 148 | */ 149 | protected function assertSchemaHasDescription(SchemaInterface $schema) { 150 | $this->assertNotEmpty($schema->getMetadata()['description']); 151 | } 152 | 153 | /** 154 | * Assert the schema has at least one property. 155 | * 156 | * More extensive property analysis would be redundant, as the only way we 157 | * could meaningfully check would be to execute the same code. This confirms 158 | * the SchemaFactory was able to derive properties at all and get them into 159 | * the schema object. 160 | * 161 | * @param \Drupal\schemata\Schema\SchemaInterface $schema 162 | * Schema to evaluate. 163 | */ 164 | protected function assertSchemaHasProperties(SchemaInterface $schema) { 165 | $this->assertGreaterThanOrEqual(1, count($schema->getProperties())); 166 | } 167 | 168 | /** 169 | * Assert the schema has the specified bundle. 170 | * 171 | * @param \Drupal\schemata\Schema\SchemaInterface $schema 172 | * Schema to evaluate. 173 | * @param string $bundle 174 | * Bundle we expect the Schema to self-declare. 175 | */ 176 | protected function assertSchemaHasBundle(SchemaInterface $schema, $bundle) { 177 | $this->assertEquals($bundle, $schema->getBundleId()); 178 | } 179 | 180 | /** 181 | * Assert the schema has no entity bundle. 182 | * 183 | * @param \Drupal\schemata\Schema\SchemaInterface $schema 184 | * Schema to evaluate. 185 | */ 186 | protected function assertSchemaHasNoBundle(SchemaInterface $schema) { 187 | $this->assertEmpty($schema->getBundleId()); 188 | } 189 | 190 | } 191 | --------------------------------------------------------------------------------