├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.sbt ├── packaging.sbt ├── project ├── build.properties ├── coverage.sbt ├── publishing.sbt └── revolver.sbt ├── src └── main │ ├── resources │ ├── application.conf │ ├── assets │ │ ├── graphiql.html │ │ └── playground.html │ ├── logback.xml │ └── reference.conf │ └── scala │ └── sangria │ └── gateway │ ├── AppConfig.scala │ ├── Main.scala │ ├── file │ ├── FileMonitor.scala │ ├── FileMonitorActor.scala │ ├── FileUtil.scala │ └── FileWatcher.scala │ ├── http │ ├── GatewayServer.scala │ ├── GraphQLRequestUnmarshaller.scala │ ├── GraphQLRouting.scala │ └── client │ │ ├── AkkaHttpClient.scala │ │ ├── HttpClient.scala │ │ └── PlayHttpClient.scala │ ├── json │ ├── CirceJsonPath.scala │ ├── CirceJsonProvider.scala │ └── CirceMappingProvider.scala │ ├── schema │ ├── CustomScalars.scala │ ├── ReloadableSchemaProvider.scala │ ├── SchemaLoader.scala │ ├── SchemaProvider.scala │ ├── StaticSchemaProvider.scala │ └── materializer │ │ ├── GatewayContext.scala │ │ ├── GatewayMaterializer.scala │ │ └── directive │ │ ├── BasicDirectiveProvider.scala │ │ ├── DirectiveProvider.scala │ │ ├── FakerDirectiveProvider.scala │ │ ├── GraphQLDirectiveProvider.scala │ │ └── HttpDirectiveProvider.scala │ └── util │ ├── LogColors.scala │ └── Logging.scala └── testSchema.graphql /.gitignore: -------------------------------------------------------------------------------- 1 | # folders 2 | .idea 3 | target 4 | lib 5 | classes 6 | 7 | # files 8 | *.iml 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.12.7 4 | jdk: 5 | - oraclejdk8 6 | 7 | script: | 8 | sbt ++$TRAVIS_SCALA_VERSION clean coverage test 9 | 10 | after_success: | 11 | sbt ++$TRAVIS_SCALA_VERSION coverageReport coveralls 12 | 13 | cache: 14 | directories: 15 | - $HOME/.ivy2/cache 16 | - $HOME/.sbt/boot/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OlegIlyenko/graphql-gateway/693861e2461337584ebefe51fb781ce36ac309ae/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **graphql-gateway** - SDL-based GraphQL gateway for REST and GraphQL-based micro-services 2 | 3 | [![Build Status](https://travis-ci.org/OlegIlyenko/graphql-gateway.svg?branch=master)](https://travis-ci.org/OlegIlyenko/graphql-gateway) 4 | [![](https://images.microbadger.com/badges/image/tenshi/graphql-gateway.svg)](https://microbadger.com/images/tenshi/graphql-gateway "Get your own image badge on microbadger.com") 5 | [![](https://images.microbadger.com/badges/version/tenshi/graphql-gateway.svg)](https://microbadger.com/images/tenshi/graphql-gateway "Get your own version badge on microbadger.com") 6 | 7 | ⚠️ **Project is a POC and at very early experimental/WIP stage!** ⚠️ 8 | 9 | ### Getting Started 10 | 11 | You can use graphql-gateway as a docker container: 12 | 13 | ```bash 14 | docker run -it -p 8080:8080 -v $(pwd):/schema tenshi/graphql-gateway 15 | ``` 16 | 17 | For you convenience, you can define an alias for it and put it in your `.bashrc`: 18 | 19 | ```bash 20 | alias graphql-gateway='docker run -it -p 8080:8080 -v $(pwd):/schema tenshi/graphql-gateway' 21 | ``` 22 | 23 | After this is done, you can start the sever just by executing `graphql-gateway` - 24 | the server will automatically read the schema from all `*.graphql` files in a current 25 | working directory and watch them for changes. 26 | 27 | There is also a fat JAR available on release notes: [graphql-gateway.jar](https://github.com/OlegIlyenko/graphql-gateway/releases/download/v0.1/graphql-gateway.jar). 28 | 29 | ### Configuration 30 | 31 | Docker container accepts a number of environment variables: 32 | 33 | * **General** 34 | * `PORT` - Int - server port (by default 8080) 35 | * `BIND_HOST` - String - bind host (by default 0.0.0.0) 36 | * `GRAPHIQL` - Boolean - enable/disable GraphiQL (by default true) 37 | * `PLAYGROUND` - Boolean - enable/disable graphql-playground which replaces GraphiQL (by default true) 38 | * `INCLUDE_DIRECTIVES` - List of String - include specific directive sets (available directive sets are: http, graphql, faker, basic) 39 | * `EXCLUDE_DIRECTIVES` - List of String - exclude specific directive sets (available directive sets are: http, graphql, faker, basic) 40 | * **Query tracing and logging** 41 | * `SLOW_LOG_ENABLED` - Boolean - enable/disable logging of slow queries (by default true) 42 | * `SLOW_LOG_THRESHOLD` - FiniteDuration - SlowLog extension threshold (by default 10 seconds) 43 | * `SLOW_LOG_EXTENSION` - Boolean - enable/disable SlowLog GraphQL extension (by default false) 44 | * `SLOW_LOG_APOLLO_TRACING` - Boolean - enable/disable Apollo tracing GraphQL extension (by default true) 45 | * **Schema live reloading** 46 | * `WATCH_ENABLED` - Boolean - enable/disable schema file reloader (by default true) 47 | * `WATCH_THRESHOLD` - FiniteDuration - internal poll interval for file watcher (by default 50 millis) 48 | * `WATCH_PATHS` - List of String - which directories to watch for schema files (by default ".") 49 | * `WATCH_GLOB` - List of String - which files are the schema files (by default "\*\*/\*.graphql") 50 | * **Limits** 51 | * `LIMIT_COMPLEXITY` - Double - query complexity limit (by default 10000) 52 | * `LIMIT_MAX_DEPTH` - Int - max query depth (by default 15) 53 | * `ALLOW_INTROSPECTION` - Boolean - enable/disable GraphQL introspect API (by default true) 54 | 55 | The full configuration can be found in the [reference.conf](https://github.com/OlegIlyenko/graphql-gateway/blob/master/src/main/resources/reference.conf). 56 | 57 | ### Supported SDL Directives 58 | 59 | Schema definition is based on [GraphQL SDL](https://github.com/facebook/graphql/pull/90). SDL syntax allows you to define full GraphQL 60 | schema with interfaces, types, enums etc. In order to provide resolution logic for the fields, you can use directives described below. 61 | Directives will define how fields will behave. By default (if no directive is provided), field resolve function will treat a contextual 62 | value as a JSON object and will return its property with the same name. (check out an [example schema](https://github.com/OlegIlyenko/graphql-gateway/blob/master/testSchema.graphql)) 63 | 64 | #### `@httpGet` 65 | 66 | ```graphql 67 | directive @httpGet( 68 | url: String!, 69 | headers: [Header!], 70 | delegateHeaders: [String!] 71 | query: [QueryParam!], 72 | forAll: String) on FIELD_DEFINITION 73 | 74 | input Header { 75 | name: String! 76 | value: String! 77 | } 78 | 79 | input QueryParam { 80 | name: String! 81 | value: String! 82 | } 83 | ``` 84 | 85 | Provides a way to resolve the field with a result of a GET HTTP request. 86 | 87 | Supports following arguments: 88 | 89 | * `url` - the URL of an HTTP request 90 | * `headers` - headers that should be sent with the request (e.g. `[{name: "Authorization", value: "Bearer FOOBARBAZ"}]`) 91 | * `query` - query string parameters that should be sent with the request (e.g. `[{name: "page-number", value: "1"}]`) 92 | * `forAll` - A [JSON Path](http://goessner.net/articles/JsonPath/) expression. For every element, returned by this expression executed against current context value, a separate HTTP request would be sent. An `elem` placeholder scope may be used in combination with this argument. 93 | 94 | `url`, `headers` and `query` may contain the placeholders which are described below. `value` directive may be used in combination with `httpGet` - it will extract part of the relevant JSON out of the HTTP response. 95 | 96 | #### `@includeSchema` 97 | 98 | ```graphql 99 | directive @includeSchema( 100 | name: String! 101 | url: String! 102 | headers: [Header!], 103 | delegateHeaders: [String!] 104 | query: [QueryParam!] 105 | oauth: OAuthClientCredentials) repeatable on SCHEMA 106 | 107 | input OAuthClientCredentials { 108 | url: String! 109 | clientId: String! 110 | clientSecret: String! 111 | scopes: [String!]! 112 | } 113 | ``` 114 | 115 | Includes external GraphQL schema (based on GraphQL endpoint URL) 116 | 117 | #### `@includeFields` 118 | 119 | ```graphql 120 | directive @includeFields( 121 | "the name of the schema included with @includeGraphQL" 122 | schema: String! 123 | 124 | "the name of the type from the external schema" 125 | type: String! 126 | 127 | "optional list of fields to include (of not provided, all fields are included)" 128 | fields: [String!] 129 | 130 | "optional list of fields to exclude" 131 | excludes: [String!]) repeatable on OBJECT 132 | ``` 133 | 134 | Adds fields loaded from the external GraphQL schema. 135 | 136 | #### `@fake` 137 | 138 | ```graphql 139 | directive @fake( 140 | expr: String, 141 | type: FakeType, 142 | min: Int, 143 | max: Int, 144 | past: Boolean, 145 | future: Boolean) on FIELD_DEFINITION 146 | ``` 147 | 148 | Provides fake data. If the field returns a list, it's size can be customized with `min`/`max`. 149 | If the field returns a `DateTime`, then `past`/`future` would be used. 150 | 151 | The fake data type can be customized with an expression `expr`. An expression has following structure: 152 | 153 | ``` 154 | #{category.name 'arg1','arg2'} 155 | ``` 156 | 157 | Here is a small example: 158 | 159 | ```graphql 160 | type Info { 161 | createAt: DateTime 162 | } 163 | 164 | type Query { 165 | name: String! @fake(expr: "name.fullName") 166 | address: String! @fake(expr: "#{address.fullAddress} - #{numerify 'SOME######'}") 167 | info: Info! @fake 168 | } 169 | ``` 170 | 171 |
172 | FakeType Enum Definition 173 | 174 | ```graphql 175 | "The type of the content generated by the faker" 176 | enum FakeType { 177 | "Represents faker expression `address.buildingNumber`." 178 | AddressBuildingNumber 179 | 180 | "Represents faker expression `address.city`." 181 | AddressCity 182 | 183 | "Represents faker expression `address.cityName`." 184 | AddressCityName 185 | 186 | "Represents faker expression `address.cityPrefix`." 187 | AddressCityPrefix 188 | 189 | "Represents faker expression `address.citySuffix`." 190 | AddressCitySuffix 191 | 192 | "Represents faker expression `address.country`." 193 | AddressCountry 194 | 195 | "Represents faker expression `address.countryCode`." 196 | AddressCountryCode 197 | 198 | "Represents faker expression `address.firstName`." 199 | AddressFirstName 200 | 201 | "Represents faker expression `address.fullAddress`." 202 | AddressFullAddress 203 | 204 | "Represents faker expression `address.lastName`." 205 | AddressLastName 206 | 207 | "Represents faker expression `address.latitude`." 208 | AddressLatitude 209 | 210 | "Represents faker expression `address.longitude`." 211 | AddressLongitude 212 | 213 | "Represents faker expression `address.secondaryAddress`." 214 | AddressSecondaryAddress 215 | 216 | "Represents faker expression `address.state`." 217 | AddressState 218 | 219 | "Represents faker expression `address.stateAbbr`." 220 | AddressStateAbbr 221 | 222 | "Represents faker expression `address.streetAddress`." 223 | AddressStreetAddress 224 | 225 | "Represents faker expression `address.streetAddressNumber`." 226 | AddressStreetAddressNumber 227 | 228 | "Represents faker expression `address.streetName`." 229 | AddressStreetName 230 | 231 | "Represents faker expression `address.streetPrefix`." 232 | AddressStreetPrefix 233 | 234 | "Represents faker expression `address.streetSuffix`." 235 | AddressStreetSuffix 236 | 237 | "Represents faker expression `address.timeZone`." 238 | AddressTimeZone 239 | 240 | "Represents faker expression `address.zipCode`." 241 | AddressZipCode 242 | 243 | "Represents faker expression `ancient.god`." 244 | AncientGod 245 | 246 | "Represents faker expression `ancient.hero`." 247 | AncientHero 248 | 249 | "Represents faker expression `ancient.primordial`." 250 | AncientPrimordial 251 | 252 | "Represents faker expression `ancient.titan`." 253 | AncientTitan 254 | 255 | "Represents faker expression `app.author`." 256 | AppAuthor 257 | 258 | "Represents faker expression `app.name`." 259 | AppName 260 | 261 | "Represents faker expression `app.version`." 262 | AppVersion 263 | 264 | "Represents faker expression `artist.name`." 265 | ArtistName 266 | 267 | "Represents faker expression `avatar.image`." 268 | AvatarImage 269 | 270 | "Represents faker expression `beer.hop`." 271 | BeerHop 272 | 273 | "Represents faker expression `beer.malt`." 274 | BeerMalt 275 | 276 | "Represents faker expression `beer.name`." 277 | BeerName 278 | 279 | "Represents faker expression `beer.style`." 280 | BeerStyle 281 | 282 | "Represents faker expression `beer.yeast`." 283 | BeerYeast 284 | 285 | "Represents faker expression `book.author`." 286 | BookAuthor 287 | 288 | "Represents faker expression `book.genre`." 289 | BookGenre 290 | 291 | "Represents faker expression `book.publisher`." 292 | BookPublisher 293 | 294 | "Represents faker expression `book.title`." 295 | BookTitle 296 | 297 | "Represents faker expression `bool.bool`." 298 | BoolBool 299 | 300 | "Represents faker expression `business.creditCardExpiry`." 301 | BusinessCreditCardExpiry 302 | 303 | "Represents faker expression `business.creditCardNumber`." 304 | BusinessCreditCardNumber 305 | 306 | "Represents faker expression `business.creditCardType`." 307 | BusinessCreditCardType 308 | 309 | "Represents faker expression `cat.breed`." 310 | CatBreed 311 | 312 | "Represents faker expression `cat.name`." 313 | CatName 314 | 315 | "Represents faker expression `cat.registry`." 316 | CatRegistry 317 | 318 | "Represents faker expression `chuckNorris.fact`." 319 | ChuckNorrisFact 320 | 321 | "Represents faker expression `code.asin`." 322 | CodeAsin 323 | 324 | "Represents faker expression `code.ean13`." 325 | CodeEan13 326 | 327 | "Represents faker expression `code.ean8`." 328 | CodeEan8 329 | 330 | "Represents faker expression `code.gtin13`." 331 | CodeGtin13 332 | 333 | "Represents faker expression `code.gtin8`." 334 | CodeGtin8 335 | 336 | "Represents faker expression `code.imei`." 337 | CodeImei 338 | 339 | "Represents faker expression `code.isbn10`." 340 | CodeIsbn10 341 | 342 | "Represents faker expression `code.isbn13`." 343 | CodeIsbn13 344 | 345 | "Represents faker expression `code.isbnGroup`." 346 | CodeIsbnGroup 347 | 348 | "Represents faker expression `code.isbnGs1`." 349 | CodeIsbnGs1 350 | 351 | "Represents faker expression `code.isbnRegistrant`." 352 | CodeIsbnRegistrant 353 | 354 | "Represents faker expression `color.name`." 355 | ColorName 356 | 357 | "Represents faker expression `commerce.color`." 358 | CommerceColor 359 | 360 | "Represents faker expression `commerce.department`." 361 | CommerceDepartment 362 | 363 | "Represents faker expression `commerce.material`." 364 | CommerceMaterial 365 | 366 | "Represents faker expression `commerce.price`." 367 | CommercePrice 368 | 369 | "Represents faker expression `commerce.productName`." 370 | CommerceProductName 371 | 372 | "Represents faker expression `commerce.promotionCode`." 373 | CommercePromotionCode 374 | 375 | "Represents faker expression `company.bs`." 376 | CompanyBs 377 | 378 | "Represents faker expression `company.buzzword`." 379 | CompanyBuzzword 380 | 381 | "Represents faker expression `company.catchPhrase`." 382 | CompanyCatchPhrase 383 | 384 | "Represents faker expression `company.industry`." 385 | CompanyIndustry 386 | 387 | "Represents faker expression `company.logo`." 388 | CompanyLogo 389 | 390 | "Represents faker expression `company.name`." 391 | CompanyName 392 | 393 | "Represents faker expression `company.profession`." 394 | CompanyProfession 395 | 396 | "Represents faker expression `company.suffix`." 397 | CompanySuffix 398 | 399 | "Represents faker expression `company.url`." 400 | CompanyUrl 401 | 402 | "Represents faker expression `crypto.md5`." 403 | CryptoMd5 404 | 405 | "Represents faker expression `crypto.sha1`." 406 | CryptoSha1 407 | 408 | "Represents faker expression `crypto.sha256`." 409 | CryptoSha256 410 | 411 | "Represents faker expression `crypto.sha512`." 412 | CryptoSha512 413 | 414 | "Represents faker expression `currency.code`." 415 | CurrencyCode 416 | 417 | "Represents faker expression `currency.name`." 418 | CurrencyName 419 | 420 | "Represents faker expression `demographic.demonym`." 421 | DemographicDemonym 422 | 423 | "Represents faker expression `demographic.educationalAttainment`." 424 | DemographicEducationalAttainment 425 | 426 | "Represents faker expression `demographic.maritalStatus`." 427 | DemographicMaritalStatus 428 | 429 | "Represents faker expression `demographic.race`." 430 | DemographicRace 431 | 432 | "Represents faker expression `demographic.sex`." 433 | DemographicSex 434 | 435 | "Represents faker expression `dog.age`." 436 | DogAge 437 | 438 | "Represents faker expression `dog.breed`." 439 | DogBreed 440 | 441 | "Represents faker expression `dog.coatLength`." 442 | DogCoatLength 443 | 444 | "Represents faker expression `dog.gender`." 445 | DogGender 446 | 447 | "Represents faker expression `dog.memePhrase`." 448 | DogMemePhrase 449 | 450 | "Represents faker expression `dog.name`." 451 | DogName 452 | 453 | "Represents faker expression `dog.size`." 454 | DogSize 455 | 456 | "Represents faker expression `dog.sound`." 457 | DogSound 458 | 459 | "Represents faker expression `dragonBall.character`." 460 | DragonBallCharacter 461 | 462 | "Represents faker expression `educator.campus`." 463 | EducatorCampus 464 | 465 | "Represents faker expression `educator.course`." 466 | EducatorCourse 467 | 468 | "Represents faker expression `educator.secondarySchool`." 469 | EducatorSecondarySchool 470 | 471 | "Represents faker expression `educator.university`." 472 | EducatorUniversity 473 | 474 | "Represents faker expression `esports.event`." 475 | EsportsEvent 476 | 477 | "Represents faker expression `esports.game`." 478 | EsportsGame 479 | 480 | "Represents faker expression `esports.league`." 481 | EsportsLeague 482 | 483 | "Represents faker expression `esports.player`." 484 | EsportsPlayer 485 | 486 | "Represents faker expression `esports.team`." 487 | EsportsTeam 488 | 489 | "Represents faker expression `file.extension`." 490 | FileExtension 491 | 492 | "Represents faker expression `file.fileName`." 493 | FileFileName 494 | 495 | "Represents faker expression `file.mimeType`." 496 | FileMimeType 497 | 498 | "Represents faker expression `finance.bic`." 499 | FinanceBic 500 | 501 | "Represents faker expression `finance.creditCard`." 502 | FinanceCreditCard 503 | 504 | "Represents faker expression `finance.iban`." 505 | FinanceIban 506 | 507 | "Represents faker expression `food.ingredient`." 508 | FoodIngredient 509 | 510 | "Represents faker expression `food.measurement`." 511 | FoodMeasurement 512 | 513 | "Represents faker expression `food.spice`." 514 | FoodSpice 515 | 516 | "Represents faker expression `friends.character`." 517 | FriendsCharacter 518 | 519 | "Represents faker expression `friends.location`." 520 | FriendsLocation 521 | 522 | "Represents faker expression `friends.quote`." 523 | FriendsQuote 524 | 525 | "Represents faker expression `funnyName.name`." 526 | FunnyNameName 527 | 528 | "Represents faker expression `gameOfThrones.character`." 529 | GameOfThronesCharacter 530 | 531 | "Represents faker expression `gameOfThrones.city`." 532 | GameOfThronesCity 533 | 534 | "Represents faker expression `gameOfThrones.dragon`." 535 | GameOfThronesDragon 536 | 537 | "Represents faker expression `gameOfThrones.house`." 538 | GameOfThronesHouse 539 | 540 | "Represents faker expression `gameOfThrones.quote`." 541 | GameOfThronesQuote 542 | 543 | "Represents faker expression `hacker.abbreviation`." 544 | HackerAbbreviation 545 | 546 | "Represents faker expression `hacker.adjective`." 547 | HackerAdjective 548 | 549 | "Represents faker expression `hacker.ingverb`." 550 | HackerIngverb 551 | 552 | "Represents faker expression `hacker.noun`." 553 | HackerNoun 554 | 555 | "Represents faker expression `hacker.verb`." 556 | HackerVerb 557 | 558 | "Represents faker expression `harryPotter.book`." 559 | HarryPotterBook 560 | 561 | "Represents faker expression `harryPotter.character`." 562 | HarryPotterCharacter 563 | 564 | "Represents faker expression `harryPotter.location`." 565 | HarryPotterLocation 566 | 567 | "Represents faker expression `harryPotter.quote`." 568 | HarryPotterQuote 569 | 570 | "Represents faker expression `hipster.word`." 571 | HipsterWord 572 | 573 | "Represents faker expression `hitchhikersGuideToTheGalaxy.character`." 574 | HitchhikersGuideToTheGalaxyCharacter 575 | 576 | "Represents faker expression `hitchhikersGuideToTheGalaxy.location`." 577 | HitchhikersGuideToTheGalaxyLocation 578 | 579 | "Represents faker expression `hitchhikersGuideToTheGalaxy.marvinQuote`." 580 | HitchhikersGuideToTheGalaxyMarvinQuote 581 | 582 | "Represents faker expression `hitchhikersGuideToTheGalaxy.planet`." 583 | HitchhikersGuideToTheGalaxyPlanet 584 | 585 | "Represents faker expression `hitchhikersGuideToTheGalaxy.quote`." 586 | HitchhikersGuideToTheGalaxyQuote 587 | 588 | "Represents faker expression `hitchhikersGuideToTheGalaxy.specie`." 589 | HitchhikersGuideToTheGalaxySpecie 590 | 591 | "Represents faker expression `hitchhikersGuideToTheGalaxy.starship`." 592 | HitchhikersGuideToTheGalaxyStarship 593 | 594 | "Represents faker expression `hobbit.character`." 595 | HobbitCharacter 596 | 597 | "Represents faker expression `hobbit.location`." 598 | HobbitLocation 599 | 600 | "Represents faker expression `hobbit.quote`." 601 | HobbitQuote 602 | 603 | "Represents faker expression `hobbit.thorinsCompany`." 604 | HobbitThorinsCompany 605 | 606 | "Represents faker expression `howIMetYourMother.catchPhrase`." 607 | HowIMetYourMotherCatchPhrase 608 | 609 | "Represents faker expression `howIMetYourMother.character`." 610 | HowIMetYourMotherCharacter 611 | 612 | "Represents faker expression `howIMetYourMother.highFive`." 613 | HowIMetYourMotherHighFive 614 | 615 | "Represents faker expression `howIMetYourMother.quote`." 616 | HowIMetYourMotherQuote 617 | 618 | "Represents faker expression `idNumber.invalid`." 619 | IdNumberInvalid 620 | 621 | "Represents faker expression `idNumber.invalidSvSeSsn`." 622 | IdNumberInvalidSvSeSsn 623 | 624 | "Represents faker expression `idNumber.ssnValid`." 625 | IdNumberSsnValid 626 | 627 | "Represents faker expression `idNumber.valid`." 628 | IdNumberValid 629 | 630 | "Represents faker expression `idNumber.validSvSeSsn`." 631 | IdNumberValidSvSeSsn 632 | 633 | "Represents faker expression `internet.avatar`." 634 | InternetAvatar 635 | 636 | "Represents faker expression `internet.domainName`." 637 | InternetDomainName 638 | 639 | "Represents faker expression `internet.domainSuffix`." 640 | InternetDomainSuffix 641 | 642 | "Represents faker expression `internet.domainWord`." 643 | InternetDomainWord 644 | 645 | "Represents faker expression `internet.emailAddress`." 646 | InternetEmailAddress 647 | 648 | "Represents faker expression `internet.image`." 649 | InternetImage 650 | 651 | "Represents faker expression `internet.ipV4Address`." 652 | InternetIpV4Address 653 | 654 | "Represents faker expression `internet.ipV4Cidr`." 655 | InternetIpV4Cidr 656 | 657 | "Represents faker expression `internet.ipV6Address`." 658 | InternetIpV6Address 659 | 660 | "Represents faker expression `internet.ipV6Cidr`." 661 | InternetIpV6Cidr 662 | 663 | "Represents faker expression `internet.macAddress`." 664 | InternetMacAddress 665 | 666 | "Represents faker expression `internet.password`." 667 | InternetPassword 668 | 669 | "Represents faker expression `internet.privateIpV4Address`." 670 | InternetPrivateIpV4Address 671 | 672 | "Represents faker expression `internet.publicIpV4Address`." 673 | InternetPublicIpV4Address 674 | 675 | "Represents faker expression `internet.safeEmailAddress`." 676 | InternetSafeEmailAddress 677 | 678 | "Represents faker expression `internet.slug`." 679 | InternetSlug 680 | 681 | "Represents faker expression `internet.url`." 682 | InternetUrl 683 | 684 | "Represents faker expression `internet.uuid`." 685 | InternetUuid 686 | 687 | "Represents faker expression `job.field`." 688 | JobField 689 | 690 | "Represents faker expression `job.keySkills`." 691 | JobKeySkills 692 | 693 | "Represents faker expression `job.position`." 694 | JobPosition 695 | 696 | "Represents faker expression `job.seniority`." 697 | JobSeniority 698 | 699 | "Represents faker expression `job.title`." 700 | JobTitle 701 | 702 | "Represents faker expression `leagueOfLegends.champion`." 703 | LeagueOfLegendsChampion 704 | 705 | "Represents faker expression `leagueOfLegends.location`." 706 | LeagueOfLegendsLocation 707 | 708 | "Represents faker expression `leagueOfLegends.masteries`." 709 | LeagueOfLegendsMasteries 710 | 711 | "Represents faker expression `leagueOfLegends.quote`." 712 | LeagueOfLegendsQuote 713 | 714 | "Represents faker expression `leagueOfLegends.rank`." 715 | LeagueOfLegendsRank 716 | 717 | "Represents faker expression `leagueOfLegends.summonerSpell`." 718 | LeagueOfLegendsSummonerSpell 719 | 720 | "Represents faker expression `lebowski.actor`." 721 | LebowskiActor 722 | 723 | "Represents faker expression `lebowski.character`." 724 | LebowskiCharacter 725 | 726 | "Represents faker expression `lebowski.quote`." 727 | LebowskiQuote 728 | 729 | "Represents faker expression `lordOfTheRings.character`." 730 | LordOfTheRingsCharacter 731 | 732 | "Represents faker expression `lordOfTheRings.location`." 733 | LordOfTheRingsLocation 734 | 735 | "Represents faker expression `lorem.character`." 736 | LoremCharacter 737 | 738 | "Represents faker expression `lorem.characters`." 739 | LoremCharacters 740 | 741 | "Represents faker expression `lorem.paragraph`." 742 | LoremParagraph 743 | 744 | "Represents faker expression `lorem.sentence`." 745 | LoremSentence 746 | 747 | "Represents faker expression `lorem.word`." 748 | LoremWord 749 | 750 | "Represents faker expression `matz.quote`." 751 | MatzQuote 752 | 753 | "Represents faker expression `music.chord`." 754 | MusicChord 755 | 756 | "Represents faker expression `music.instrument`." 757 | MusicInstrument 758 | 759 | "Represents faker expression `music.key`." 760 | MusicKey 761 | 762 | "Represents faker expression `name.firstName`." 763 | NameFirstName 764 | 765 | "Represents faker expression `name.fullName`." 766 | NameFullName 767 | 768 | "Represents faker expression `name.lastName`." 769 | NameLastName 770 | 771 | "Represents faker expression `name.name`." 772 | NameName 773 | 774 | "Represents faker expression `name.nameWithMiddle`." 775 | NameNameWithMiddle 776 | 777 | "Represents faker expression `name.prefix`." 778 | NamePrefix 779 | 780 | "Represents faker expression `name.suffix`." 781 | NameSuffix 782 | 783 | "Represents faker expression `name.title`." 784 | NameTitle 785 | 786 | "Represents faker expression `name.username`." 787 | NameUsername 788 | 789 | "Represents faker expression `number.digit`." 790 | NumberDigit 791 | 792 | "Represents faker expression `number.randomDigit`." 793 | NumberRandomDigit 794 | 795 | "Represents faker expression `number.randomDigitNotZero`." 796 | NumberRandomDigitNotZero 797 | 798 | "Represents faker expression `number.randomNumber`." 799 | NumberRandomNumber 800 | 801 | "Represents faker expression `overwatch.hero`." 802 | OverwatchHero 803 | 804 | "Represents faker expression `overwatch.location`." 805 | OverwatchLocation 806 | 807 | "Represents faker expression `overwatch.quote`." 808 | OverwatchQuote 809 | 810 | "Represents faker expression `phoneNumber.cellPhone`." 811 | PhoneNumberCellPhone 812 | 813 | "Represents faker expression `phoneNumber.phoneNumber`." 814 | PhoneNumberPhoneNumber 815 | 816 | "Represents faker expression `pokemon.location`." 817 | PokemonLocation 818 | 819 | "Represents faker expression `pokemon.name`." 820 | PokemonName 821 | 822 | "Represents faker expression `random.nextDouble`." 823 | RandomNextDouble 824 | 825 | "Represents faker expression `random.nextLong`." 826 | RandomNextLong 827 | 828 | "Represents faker expression `rickAndMorty.character`." 829 | RickAndMortyCharacter 830 | 831 | "Represents faker expression `rickAndMorty.location`." 832 | RickAndMortyLocation 833 | 834 | "Represents faker expression `rickAndMorty.quote`." 835 | RickAndMortyQuote 836 | 837 | "Represents faker expression `robin.quote`." 838 | RobinQuote 839 | 840 | "Represents faker expression `rockBand.name`." 841 | RockBandName 842 | 843 | "Represents faker expression `shakespeare.asYouLikeItQuote`." 844 | ShakespeareAsYouLikeItQuote 845 | 846 | "Represents faker expression `shakespeare.hamletQuote`." 847 | ShakespeareHamletQuote 848 | 849 | "Represents faker expression `shakespeare.kingRichardIIIQuote`." 850 | ShakespeareKingRichardIIIQuote 851 | 852 | "Represents faker expression `shakespeare.romeoAndJulietQuote`." 853 | ShakespeareRomeoAndJulietQuote 854 | 855 | "Represents faker expression `slackEmoji.activity`." 856 | SlackEmojiActivity 857 | 858 | "Represents faker expression `slackEmoji.celebration`." 859 | SlackEmojiCelebration 860 | 861 | "Represents faker expression `slackEmoji.custom`." 862 | SlackEmojiCustom 863 | 864 | "Represents faker expression `slackEmoji.emoji`." 865 | SlackEmojiEmoji 866 | 867 | "Represents faker expression `slackEmoji.foodAndDrink`." 868 | SlackEmojiFoodAndDrink 869 | 870 | "Represents faker expression `slackEmoji.nature`." 871 | SlackEmojiNature 872 | 873 | "Represents faker expression `slackEmoji.objectsAndSymbols`." 874 | SlackEmojiObjectsAndSymbols 875 | 876 | "Represents faker expression `slackEmoji.people`." 877 | SlackEmojiPeople 878 | 879 | "Represents faker expression `slackEmoji.travelAndPlaces`." 880 | SlackEmojiTravelAndPlaces 881 | 882 | "Represents faker expression `space.agency`." 883 | SpaceAgency 884 | 885 | "Represents faker expression `space.agencyAbbreviation`." 886 | SpaceAgencyAbbreviation 887 | 888 | "Represents faker expression `space.company`." 889 | SpaceCompany 890 | 891 | "Represents faker expression `space.constellation`." 892 | SpaceConstellation 893 | 894 | "Represents faker expression `space.distanceMeasurement`." 895 | SpaceDistanceMeasurement 896 | 897 | "Represents faker expression `space.galaxy`." 898 | SpaceGalaxy 899 | 900 | "Represents faker expression `space.meteorite`." 901 | SpaceMeteorite 902 | 903 | "Represents faker expression `space.moon`." 904 | SpaceMoon 905 | 906 | "Represents faker expression `space.nasaSpaceCraft`." 907 | SpaceNasaSpaceCraft 908 | 909 | "Represents faker expression `space.nebula`." 910 | SpaceNebula 911 | 912 | "Represents faker expression `space.planet`." 913 | SpacePlanet 914 | 915 | "Represents faker expression `space.star`." 916 | SpaceStar 917 | 918 | "Represents faker expression `space.starCluster`." 919 | SpaceStarCluster 920 | 921 | "Represents faker expression `starTrek.character`." 922 | StarTrekCharacter 923 | 924 | "Represents faker expression `starTrek.location`." 925 | StarTrekLocation 926 | 927 | "Represents faker expression `starTrek.specie`." 928 | StarTrekSpecie 929 | 930 | "Represents faker expression `starTrek.villain`." 931 | StarTrekVillain 932 | 933 | "Represents faker expression `stock.nsdqSymbol`." 934 | StockNsdqSymbol 935 | 936 | "Represents faker expression `stock.nyseSymbol`." 937 | StockNyseSymbol 938 | 939 | "Represents faker expression `superhero.descriptor`." 940 | SuperheroDescriptor 941 | 942 | "Represents faker expression `superhero.name`." 943 | SuperheroName 944 | 945 | "Represents faker expression `superhero.power`." 946 | SuperheroPower 947 | 948 | "Represents faker expression `superhero.prefix`." 949 | SuperheroPrefix 950 | 951 | "Represents faker expression `superhero.suffix`." 952 | SuperheroSuffix 953 | 954 | "Represents faker expression `team.creature`." 955 | TeamCreature 956 | 957 | "Represents faker expression `team.name`." 958 | TeamName 959 | 960 | "Represents faker expression `team.sport`." 961 | TeamSport 962 | 963 | "Represents faker expression `team.state`." 964 | TeamState 965 | 966 | "Represents faker expression `twinPeaks.character`." 967 | TwinPeaksCharacter 968 | 969 | "Represents faker expression `twinPeaks.location`." 970 | TwinPeaksLocation 971 | 972 | "Represents faker expression `twinPeaks.quote`." 973 | TwinPeaksQuote 974 | 975 | "Represents faker expression `university.name`." 976 | UniversityName 977 | 978 | "Represents faker expression `university.prefix`." 979 | UniversityPrefix 980 | 981 | "Represents faker expression `university.suffix`." 982 | UniversitySuffix 983 | 984 | "Represents faker expression `weather.description`." 985 | WeatherDescription 986 | 987 | "Represents faker expression `weather.temperatureCelsius`." 988 | WeatherTemperatureCelsius 989 | 990 | "Represents faker expression `weather.temperatureFahrenheit`." 991 | WeatherTemperatureFahrenheit 992 | 993 | "Represents faker expression `witcher.character`." 994 | WitcherCharacter 995 | 996 | "Represents faker expression `witcher.location`." 997 | WitcherLocation 998 | 999 | "Represents faker expression `witcher.monster`." 1000 | WitcherMonster 1001 | 1002 | "Represents faker expression `witcher.quote`." 1003 | WitcherQuote 1004 | 1005 | "Represents faker expression `witcher.school`." 1006 | WitcherSchool 1007 | 1008 | "Represents faker expression `witcher.witcher`." 1009 | WitcherWitcher 1010 | 1011 | "Represents faker expression `yoda.quote`." 1012 | YodaQuote 1013 | 1014 | "Represents faker expression `zelda.character`." 1015 | ZeldaCharacter 1016 | 1017 | "Represents faker expression `zelda.game`." 1018 | ZeldaGame 1019 | } 1020 | ``` 1021 |
1022 | 1023 |
1024 | Full list of all available faker expressions 1025 | 1026 | * `bothify(string: String)` 1027 | 1028 | Applies both a numerify(String) and a letterify(String) over the incoming string. 1029 | * `bothify(string: String, isUpper: Boolean)` 1030 | 1031 | Applies both a numerify(String) and a letterify(String) over the incoming string. 1032 | * `letterify(letterString: String, isUpper: Boolean)` 1033 | 1034 | Returns a string with the '?' characters in the parameter replaced with random alphabetic characters. For example, the string "12??34" could be replaced with a string like "12AB34". 1035 | * `letterify(letterString: String)` 1036 | 1037 | Returns a string with the '?' characters in the parameter replaced with random alphabetic characters. For example, the string "12??34" could be replaced with a string like "12AB34". 1038 | * `numerify(numberString: String)` 1039 | 1040 | Returns a string with the '#' characters in the parameter replaced with random digits between 0-9 inclusive. For example, the string "ABC##EFG" could be replaced with a string like "ABC99EFG". 1041 | * `regexify(regex: String)` 1042 | 1043 | Generates a String that matches the given regular expression. 1044 | * `resolve(key: String)` 1045 | * `address` 1046 | * `address.buildingNumber` 1047 | * `address.city` 1048 | * `address.cityName` 1049 | * `address.cityPrefix` 1050 | * `address.citySuffix` 1051 | * `address.country` 1052 | * `address.countryCode` 1053 | * `address.firstName` 1054 | * `address.fullAddress` 1055 | * `address.lastName` 1056 | * `address.latitude` 1057 | * `address.longitude` 1058 | * `address.secondaryAddress` 1059 | * `address.state` 1060 | * `address.stateAbbr` 1061 | * `address.streetAddress` 1062 | * `address.streetAddress(includeSecondary: Boolean)` 1063 | * `address.streetAddressNumber` 1064 | * `address.streetName` 1065 | * `address.streetPrefix` 1066 | * `address.streetSuffix` 1067 | * `address.timeZone` 1068 | * `address.zipCode` 1069 | * `address.zipCodeByState(stateAbbr: String)` 1070 | * `ancient` 1071 | * `ancient.god` 1072 | * `ancient.hero` 1073 | * `ancient.primordial` 1074 | * `ancient.titan` 1075 | * `app` 1076 | * `app.author` 1077 | * `app.name` 1078 | * `app.version` 1079 | * `artist` 1080 | * `artist.name` 1081 | * `avatar` 1082 | * `avatar.image` 1083 | * `beer` 1084 | * `beer.hop` 1085 | * `beer.malt` 1086 | * `beer.name` 1087 | * `beer.style` 1088 | * `beer.yeast` 1089 | * `book` 1090 | * `book.author` 1091 | * `book.genre` 1092 | * `book.publisher` 1093 | * `book.title` 1094 | * `bool` 1095 | * `bool.bool` 1096 | * `business` 1097 | * `business.creditCardExpiry` 1098 | * `business.creditCardNumber` 1099 | * `business.creditCardType` 1100 | * `cat` 1101 | * `cat.breed` 1102 | * `cat.name` 1103 | * `cat.registry` 1104 | * `chuckNorris` 1105 | * `chuckNorris.fact` 1106 | * `code` 1107 | * `code.asin` 1108 | * `code.ean13` 1109 | * `code.ean8` 1110 | * `code.gtin13` 1111 | * `code.gtin8` 1112 | * `code.imei` 1113 | * `code.isbn10` 1114 | * `code.isbn10(separator: Boolean)` 1115 | * `code.isbn13` 1116 | * `code.isbn13(separator: Boolean)` 1117 | * `code.isbnGroup` 1118 | 1119 | This can be overridden by specifying code: isbn_group: "some expression" in the appropriate yml file. 1120 | * `code.isbnGs1` 1121 | 1122 | This can be overridden by specifying code: isbn_gs1: "some expression" in the appropriate yml file. 1123 | * `code.isbnRegistrant` 1124 | 1125 | This can be overridden by specifying code: isbn_registrant: "some expression" in the appropriate yml file. 1126 | * `color` 1127 | * `color.name` 1128 | * `commerce` 1129 | * `commerce.color` 1130 | * `commerce.department` 1131 | * `commerce.material` 1132 | * `commerce.price(min: Double, max: Double)` 1133 | * `commerce.price` 1134 | 1135 | Generate a random price between 0.00 and 100.00 1136 | * `commerce.productName` 1137 | * `commerce.promotionCode` 1138 | * `commerce.promotionCode(digits: Int)` 1139 | * `company` 1140 | * `company.bs` 1141 | 1142 | When a straight answer won't do, BS to the rescue! 1143 | * `company.buzzword` 1144 | * `company.catchPhrase` 1145 | 1146 | Generate a buzzword-laden catch phrase. 1147 | * `company.industry` 1148 | * `company.logo` 1149 | 1150 | Generate a random company logo url in PNG format. 1151 | * `company.name` 1152 | * `company.profession` 1153 | * `company.suffix` 1154 | * `company.url` 1155 | * `crypto` 1156 | * `crypto.md5` 1157 | * `crypto.sha1` 1158 | * `crypto.sha256` 1159 | * `crypto.sha512` 1160 | * `currency` 1161 | * `currency.code` 1162 | * `currency.name` 1163 | * `date` 1164 | * `demographic` 1165 | * `demographic.demonym` 1166 | * `demographic.educationalAttainment` 1167 | * `demographic.maritalStatus` 1168 | * `demographic.race` 1169 | * `demographic.sex` 1170 | * `dog` 1171 | * `dog.age` 1172 | * `dog.breed` 1173 | * `dog.coatLength` 1174 | * `dog.gender` 1175 | * `dog.memePhrase` 1176 | * `dog.name` 1177 | * `dog.size` 1178 | * `dog.sound` 1179 | * `dragonBall` 1180 | * `dragonBall.character` 1181 | * `educator` 1182 | * `educator.campus` 1183 | * `educator.course` 1184 | * `educator.secondarySchool` 1185 | * `educator.university` 1186 | * `esports` 1187 | * `esports.event` 1188 | * `esports.game` 1189 | * `esports.league` 1190 | * `esports.player` 1191 | * `esports.team` 1192 | * `file` 1193 | * `file.extension` 1194 | * `file.fileName` 1195 | * `file.fileName(dirOrNull: String, nameOrNull: String, extensionOrNull: String, separatorOrNull: String)` 1196 | * `file.mimeType` 1197 | * `finance` 1198 | * `finance.bic` 1199 | 1200 | Generates a random Business Identifier Code 1201 | * `finance.creditCard` 1202 | * `finance.creditCard(creditCardType: CreditCardType)` 1203 | * `finance.iban` 1204 | * `finance.iban(countryCode: String)` 1205 | * `food` 1206 | * `food.ingredient` 1207 | * `food.measurement` 1208 | * `food.spice` 1209 | * `friends` 1210 | * `friends.character` 1211 | * `friends.location` 1212 | * `friends.quote` 1213 | * `funnyName` 1214 | * `funnyName.name` 1215 | * `gameOfThrones` 1216 | * `gameOfThrones.character` 1217 | * `gameOfThrones.city` 1218 | * `gameOfThrones.dragon` 1219 | * `gameOfThrones.house` 1220 | * `gameOfThrones.quote` 1221 | * `hacker` 1222 | * `hacker.abbreviation` 1223 | * `hacker.adjective` 1224 | * `hacker.ingverb` 1225 | * `hacker.noun` 1226 | * `hacker.verb` 1227 | * `harryPotter` 1228 | * `harryPotter.book` 1229 | * `harryPotter.character` 1230 | * `harryPotter.location` 1231 | * `harryPotter.quote` 1232 | * `hipster` 1233 | * `hipster.word` 1234 | * `hitchhikersGuideToTheGalaxy` 1235 | * `hitchhikersGuideToTheGalaxy.character` 1236 | * `hitchhikersGuideToTheGalaxy.location` 1237 | * `hitchhikersGuideToTheGalaxy.marvinQuote` 1238 | * `hitchhikersGuideToTheGalaxy.planet` 1239 | * `hitchhikersGuideToTheGalaxy.quote` 1240 | * `hitchhikersGuideToTheGalaxy.specie` 1241 | * `hitchhikersGuideToTheGalaxy.starship` 1242 | * `hobbit` 1243 | * `hobbit.character` 1244 | * `hobbit.location` 1245 | * `hobbit.quote` 1246 | * `hobbit.thorinsCompany` 1247 | * `howIMetYourMother` 1248 | * `howIMetYourMother.catchPhrase` 1249 | * `howIMetYourMother.character` 1250 | * `howIMetYourMother.highFive` 1251 | * `howIMetYourMother.quote` 1252 | * `idNumber` 1253 | * `idNumber.invalid` 1254 | * `idNumber.invalidSvSeSsn` 1255 | 1256 | Specified as #{IDNumber.invalid_sv_se_ssn} in sv-SE.yml 1257 | * `idNumber.ssnValid` 1258 | * `idNumber.valid` 1259 | * `idNumber.validSvSeSsn` 1260 | 1261 | Specified as #{IDNumber.valid_sv_se_ssn} in sv-SE.yml 1262 | * `internet` 1263 | * `internet.avatar` 1264 | 1265 | Generates a random avatar url based on a collection of profile pictures of real people. All this avatar have been authorized by its awesome users to be used on live websites (not just mockups). For more information, please visit: http://uifaces.com/authorized 1266 | * `internet.domainName` 1267 | * `internet.domainSuffix` 1268 | * `internet.domainWord` 1269 | * `internet.emailAddress` 1270 | * `internet.emailAddress(localPart: String)` 1271 | * `internet.image` 1272 | 1273 | Generates a random image url based on the lorempixel service. All the images provided by this service are released under the creative commons license (CC BY-SA). For more information, please visit: http://lorempixel.com/ 1274 | * `internet.image(width: Int, height: Int, gray: java.lang.Boolean, text: String)` 1275 | 1276 | Same as image() but allows client code to choose a few image characteristics 1277 | * `internet.ipV4Address` 1278 | 1279 | returns an IPv4 address in dot separated octets. 1280 | * `internet.ipV4Cidr` 1281 | * `internet.ipV6Address` 1282 | 1283 | Returns an IPv6 address in hh:hh:hh:hh:hh:hh:hh:hh format. 1284 | * `internet.ipV6Cidr` 1285 | * `internet.macAddress(prefix: String)` 1286 | 1287 | Returns a MAC address in the following format: 6-bytes in MM:MM:MM:SS:SS:SS format. 1288 | * `internet.macAddress` 1289 | * `internet.password(minimumLength: Int, maximumLength: Int, includeUppercase: Boolean, includeSpecial: Boolean)` 1290 | * `internet.password(minimumLength: Int, maximumLength: Int, includeUppercase: Boolean)` 1291 | * `internet.password` 1292 | * `internet.password(minimumLength: Int, maximumLength: Int)` 1293 | * `internet.privateIpV4Address` 1294 | * `internet.publicIpV4Address` 1295 | * `internet.safeEmailAddress(localPart: String)` 1296 | * `internet.safeEmailAddress` 1297 | * `internet.slug(wordsOrNull: [String], glueOrNull: String)` 1298 | * `internet.slug` 1299 | * `internet.url` 1300 | * `internet.uuid` 1301 | 1302 | Returns a UUID (type 4) as String. 1303 | * `job` 1304 | * `job.field` 1305 | * `job.keySkills` 1306 | * `job.position` 1307 | * `job.seniority` 1308 | * `job.title` 1309 | * `leagueOfLegends` 1310 | * `leagueOfLegends.champion` 1311 | * `leagueOfLegends.location` 1312 | * `leagueOfLegends.masteries` 1313 | * `leagueOfLegends.quote` 1314 | * `leagueOfLegends.rank` 1315 | * `leagueOfLegends.summonerSpell` 1316 | * `lebowski` 1317 | * `lebowski.actor` 1318 | * `lebowski.character` 1319 | * `lebowski.quote` 1320 | * `lordOfTheRings` 1321 | * `lordOfTheRings.character` 1322 | * `lordOfTheRings.location` 1323 | * `lorem` 1324 | * `lorem.character(includeUppercase: Boolean)` 1325 | * `lorem.character` 1326 | * `lorem.characters(includeUppercase: Boolean)` 1327 | * `lorem.characters(minimumLength: Int, maximumLength: Int, includeUppercase: Boolean)` 1328 | * `lorem.characters(minimumLength: Int, maximumLength: Int)` 1329 | * `lorem.characters` 1330 | * `lorem.characters(includeUppercase: Boolean)` 1331 | * `lorem.characters(minimumLength: Int, maximumLength: Int)` 1332 | * `lorem.fixedString(numberOfLetters: Int)` 1333 | 1334 | Create a string with a fixed size. Can be useful for testing validator based on length string for example 1335 | * `lorem.paragraph(sentenceCount: Int)` 1336 | * `lorem.paragraph` 1337 | * `lorem.sentence(wordCount: Int)` 1338 | 1339 | Create a sentence with a random number of words within the range (wordCount+1)..(wordCount+6). 1340 | * `lorem.sentence` 1341 | 1342 | Create a sentence with a random number of words within the range 4..10. 1343 | * `lorem.sentence(wordCount: Int, randomWordsToAdd: Int)` 1344 | 1345 | Create a sentence with a random number of words within the range (wordCount+1)..(wordCount+randomWordsToAdd). Set randomWordsToAdd to 0 to generate sentences with a fixed number of words. 1346 | * `lorem.word` 1347 | * `matz` 1348 | * `matz.quote` 1349 | * `music` 1350 | * `music.chord` 1351 | * `music.instrument` 1352 | * `music.key` 1353 | * `name` 1354 | * `name.firstName` 1355 | 1356 | Returns a random 'given' name such as Aaliyah, Aaron, Abagail or Abbey 1357 | * `name.fullName` 1358 | 1359 | Returns the same value as name() 1360 | * `name.lastName` 1361 | 1362 | Returns a random last name such as Smith, Jones or Baldwin 1363 | * `name.name` 1364 | 1365 | A multipart name composed of an optional prefix, a firstname and a lastname or other possible variances based on locale. Examples: James Jones Jr. Julie Johnson 1366 | * `name.nameWithMiddle` 1367 | 1368 | A multipart name composed of an optional prefix, a given and family name, another 'firstname' for the middle name and an optional suffix such as Jr. Examples: Mrs. Ella Geraldine Fitzgerald Jason Tom Sawyer Jr. Helen Jessica Troy 1369 | * `name.prefix` 1370 | 1371 | Returns a name prefix such as Mr., Mrs., Ms., Miss, or Dr. 1372 | * `name.suffix` 1373 | 1374 | Returns a name suffix such as Jr., Sr., I, II, III, IV, V, MD, DDS, PhD or DVM 1375 | * `name.title` 1376 | 1377 | A three part title composed of a descriptor level and job. Some examples are : (template) {descriptor} {level} {job} Lead Solutions Specialist National Marketing Manager Central Response Liaison 1378 | * `name.username` 1379 | 1380 | A lowercase username composed of the first_name and last_name joined with a '.'. Some examples are: (template) firstName().lastName() jim.jones jason.leigh tracy.jordan 1381 | * `number` 1382 | * `number.digit` 1383 | * `number.digits(count: Int)` 1384 | * `number.numberBetween(min: Int, max: Int)` 1385 | * `number.numberBetween(min: Int, max: Int)` 1386 | * `number.randomDigit` 1387 | 1388 | Returns a random number from 0-9 (both inclusive) 1389 | * `number.randomDigitNotZero` 1390 | 1391 | Returns a random number from 1-9 (both inclusive) 1392 | * `number.randomDouble(maxNumberOfDecimals: Int, min: Int, max: Int)` 1393 | * `number.randomDouble(maxNumberOfDecimals: Int, min: Int, max: Int)` 1394 | * `number.randomNumber` 1395 | 1396 | Returns a random number 1397 | * `number.randomNumber(numberOfDigits: Int, strict: Boolean)` 1398 | * `options` 1399 | * `overwatch` 1400 | * `overwatch.hero` 1401 | * `overwatch.location` 1402 | * `overwatch.quote` 1403 | * `phoneNumber` 1404 | * `phoneNumber.cellPhone` 1405 | * `phoneNumber.phoneNumber` 1406 | * `pokemon` 1407 | * `pokemon.location` 1408 | * `pokemon.name` 1409 | * `random` 1410 | * `random.nextDouble` 1411 | * `random.nextInt(min: Int, max: Int)` 1412 | * `random.nextInt(n: Int)` 1413 | * `random.nextLong(n: long)` 1414 | * `random.nextLong` 1415 | * `rickAndMorty` 1416 | * `rickAndMorty.character` 1417 | * `rickAndMorty.location` 1418 | * `rickAndMorty.quote` 1419 | * `robin` 1420 | * `robin.quote` 1421 | * `rockBand` 1422 | * `rockBand.name` 1423 | * `shakespeare` 1424 | * `shakespeare.asYouLikeItQuote` 1425 | * `shakespeare.hamletQuote` 1426 | * `shakespeare.kingRichardIIIQuote` 1427 | * `shakespeare.romeoAndJulietQuote` 1428 | * `slackEmoji` 1429 | * `slackEmoji.activity` 1430 | * `slackEmoji.celebration` 1431 | * `slackEmoji.custom` 1432 | * `slackEmoji.emoji` 1433 | * `slackEmoji.foodAndDrink` 1434 | * `slackEmoji.nature` 1435 | * `slackEmoji.objectsAndSymbols` 1436 | * `slackEmoji.people` 1437 | * `slackEmoji.travelAndPlaces` 1438 | * `space` 1439 | * `space.agency` 1440 | * `space.agencyAbbreviation` 1441 | * `space.company` 1442 | * `space.constellation` 1443 | * `space.distanceMeasurement` 1444 | * `space.galaxy` 1445 | * `space.meteorite` 1446 | * `space.moon` 1447 | * `space.nasaSpaceCraft` 1448 | * `space.nebula` 1449 | * `space.planet` 1450 | * `space.star` 1451 | * `space.starCluster` 1452 | * `starTrek` 1453 | * `starTrek.character` 1454 | * `starTrek.location` 1455 | * `starTrek.specie` 1456 | * `starTrek.villain` 1457 | * `stock` 1458 | * `stock.nsdqSymbol` 1459 | * `stock.nyseSymbol` 1460 | * `superhero` 1461 | * `superhero.descriptor` 1462 | * `superhero.name` 1463 | * `superhero.power` 1464 | * `superhero.prefix` 1465 | * `superhero.suffix` 1466 | * `team` 1467 | * `team.creature` 1468 | * `team.name` 1469 | * `team.sport` 1470 | * `team.state` 1471 | * `twinPeaks` 1472 | * `twinPeaks.character` 1473 | * `twinPeaks.location` 1474 | * `twinPeaks.quote` 1475 | * `university` 1476 | * `university.name` 1477 | * `university.prefix` 1478 | * `university.suffix` 1479 | * `weather` 1480 | * `weather.description` 1481 | 1482 | Generates a short weather description. 1483 | * `weather.temperatureCelsius(minTemperature: Int, maxTemperature: Int)` 1484 | 1485 | Generates a random temperature celsius between two temperatures. 1486 | * `weather.temperatureCelsius` 1487 | 1488 | Generates a random temperature celsius between -30 and 38 degrees. 1489 | * `weather.temperatureFahrenheit` 1490 | 1491 | Generates a random temperature fahrenheit between -22 and 100 degrees. 1492 | * `weather.temperatureFahrenheit(minTemperature: Int, maxTemperature: Int)` 1493 | 1494 | Generates a random temperature fahrenheit between two temperatures. 1495 | * `witcher` 1496 | * `witcher.character` 1497 | * `witcher.location` 1498 | * `witcher.monster` 1499 | * `witcher.quote` 1500 | * `witcher.school` 1501 | * `witcher.witcher` 1502 | * `yoda` 1503 | * `yoda.quote` 1504 | * `zelda` 1505 | * `zelda.character` 1506 | * `zelda.game` 1507 |
1508 | 1509 | #### `@fakeConfig` 1510 | 1511 | ```graphql 1512 | directive @fakeConfig(locale: String, seed: Int) on SCHEMA 1513 | ``` 1514 | 1515 | Customize fake data generation 1516 | 1517 | #### `@const` 1518 | 1519 | ```graphql 1520 | directive @const(value: Any!) on FIELD_DEFINITION | SCHEMA 1521 | ``` 1522 | 1523 | Provides a way to resolve a field with a constant value. `value` can be any valid GraphQL input value. It would be treated as a JSON value. 1524 | 1525 | #### `@jsonConst` 1526 | 1527 | ```graphql 1528 | directive @jsonConst(value: String!) on FIELD_DEFINITION | SCHEMA 1529 | ``` 1530 | 1531 | Provides a way to resolve a field with a constant value. `value` should be a valid JSON value. 1532 | 1533 | #### `@arg` 1534 | 1535 | ```graphql 1536 | directive @arg(name: String!) on FIELD_DEFINITION 1537 | ``` 1538 | 1539 | Provides a way to resolve a field with value of one of its arguments. 1540 | 1541 | #### `@value` 1542 | 1543 | ```graphql 1544 | directive @value(name: String, path: String) on FIELD_DEFINITION 1545 | ``` 1546 | 1547 | Extracts a value(s) from the context object. It supports following extractors via arguments (only one can be used): 1548 | 1549 | * `name` - Extracts a named property value from a context JSON object 1550 | * `path` - A [JSON Path](http://goessner.net/articles/JsonPath/) expression. It would be executed against current context JSON value. 1551 | 1552 | #### `@context` 1553 | 1554 | ```graphql 1555 | directive @context(name: String, path: String) on FIELD_DEFINITION 1556 | ``` 1557 | 1558 | Extracts a value(s) from the context object defined on the schema level. It supports following extractors via arguments (only one can be used): 1559 | 1560 | * `name` - Extracts a named property value from a JSON object 1561 | * `path` - A [JSON Path](http://goessner.net/articles/JsonPath/) expression. It would be executed against current context JSON value, which is defined at the schema level. 1562 | 1563 | ### Placeholders 1564 | 1565 | Placeholders may be used in some the directive arguments (inside of the strings) and the syntax looks like this: 1566 | 1567 | ``` 1568 | ${value.$.results[0].film} 1569 | ``` 1570 | 1571 | The placeholder consists of two parts separated by dot (`.`): the scope (`value` in this case) and the extractor (`$.results[0].film`) - a JSON Path extractor in this example). The scope defines a place/value from which you would like extract a value. Following scopes are supported: 1572 | 1573 | * `arg` - field argument 1574 | * `value` - a context value 1575 | * `ctx` - a context value which is defined on a schema level 1576 | * `elem` - an extracted element that comes from the `forAll` argument 1577 | * `env` - an environment variable 1578 | 1579 | The extractor can be either a string (the name of the property) or a [JSON Path](http://goessner.net/articles/JsonPath/) expression. 1580 | 1581 | ### Setup 1582 | 1583 | **Prerequisites** 1584 | 1585 | * [Java 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) 1586 | * [SBT](http://www.scala-sbt.org/download.html) 1587 | 1588 | **Running** 1589 | 1590 | After starting the server with 1591 | 1592 | ```bash 1593 | sbt run 1594 | 1595 | # or, if you want to watch the source code changes 1596 | 1597 | sbt ~reStart 1598 | ``` 1599 | 1600 | you can run queries interactively using [GraphiQL](https://github.com/graphql/graphiql) by opening [http://localhost:8080](http://localhost:8080) in a browser or query the `/graphql` endpoint directly. 1601 | 1602 | **Publishing** 1603 | 1604 | The docker publishing is done with sbt-native-packager plugin: 1605 | 1606 | ```bash 1607 | sbt docker:publishLocal 1608 | 1609 | # or 1610 | 1611 | sbt docker:publish 1612 | ``` 1613 | 1614 | Fat JAR is created with sbt-assembly plugin: 1615 | 1616 | ```bash 1617 | sbt assembly 1618 | ``` 1619 | 1620 | ## License 1621 | 1622 | **graphql-gateway** is licensed under [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). 1623 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "graphql-gateway" 2 | organization := "org.sangria-graphql" 3 | version := "0.1.0-SNAPSHOT" 4 | 5 | description := "GraphQL Gateway - SDL-based GraphQL gateway for REST and GraphQL-based microservices" 6 | homepage := Some(url("http://sangria-graphql.org")) 7 | licenses := Seq("Apache License, ASL Version 2.0" → url("http://www.apache.org/licenses/LICENSE-2.0")) 8 | 9 | scalaVersion := "2.12.7" 10 | 11 | scalacOptions ++= Seq("-deprecation", "-feature") 12 | 13 | mainClass in Compile := Some("sangria.gateway.Main") 14 | 15 | val sangriaVersion = "1.4.3-SNAPSHOT" 16 | val circeVersion = "0.10.0" 17 | 18 | libraryDependencies ++= Seq( 19 | "org.sangria-graphql" %% "sangria" % sangriaVersion, 20 | "org.sangria-graphql" %% "sangria-slowlog" % "0.1.8", 21 | "org.sangria-graphql" %% "sangria-circe" % "1.2.1", 22 | 23 | "com.typesafe.akka" %% "akka-http" % "10.1.5", 24 | "com.typesafe.akka" %% "akka-slf4j" % "2.5.17", 25 | "de.heikoseeberger" %% "akka-http-circe" % "1.22.0", 26 | "de.heikoseeberger" %% "akka-sse" % "3.0.0", 27 | 28 | "com.github.pathikrit" %% "better-files-akka" % "3.6.0", 29 | 30 | "io.circe" %% "circe-core" % circeVersion, 31 | "io.circe" %% "circe-generic" % circeVersion, 32 | "io.circe" %% "circe-parser" % circeVersion, 33 | "io.circe" %% "circe-optics" % circeVersion, 34 | 35 | "com.jayway.jsonpath" % "json-path" % "2.4.0", 36 | 37 | "com.typesafe.play" %% "play-ahc-ws-standalone" % "1.1.10", 38 | 39 | "com.iheart" %% "ficus" % "1.4.3", 40 | "com.github.javafaker" % "javafaker" % "0.16", 41 | "info.henix" %% "ssoup" % "0.5", 42 | 43 | "org.slf4j" % "slf4j-api" % "1.7.25", 44 | "ch.qos.logback" % "logback-classic" % "1.2.3", 45 | "com.typesafe.scala-logging" %% "scala-logging" % "3.9.0", 46 | 47 | "org.scalatest" %% "scalatest" % "3.0.5" % Test 48 | ) 49 | 50 | resolvers += "Sonatype snapshots" at "https://oss.sonatype.org/content/repositories/snapshots/" 51 | 52 | // nice *magenta* prompt! 53 | 54 | shellPrompt in ThisBuild := { state ⇒ 55 | scala.Console.MAGENTA + Project.extract(state).currentRef.project + "> " + scala.Console.RESET 56 | } 57 | -------------------------------------------------------------------------------- /packaging.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.sbt.packager.docker.Cmd 2 | 3 | import scala.sys.process._ 4 | 5 | dockerBaseImage := "frolvlad/alpine-oraclejdk8:8.161.12-slim" 6 | version in Docker := ("git rev-parse HEAD" !!).trim 7 | dockerRepository := Some("tenshi") 8 | dockerUpdateLatest := true 9 | dockerExposedVolumes := Seq(s"/schema") 10 | dockerExposedPorts := Seq(8080) 11 | 12 | dockerCommands := Seq( 13 | dockerCommands.value.head, 14 | // Install bash to be able to start the application 15 | Cmd("RUN apk add --update bash && rm -rf /var/cache/apk/*") 16 | ) ++ dockerCommands.value.tail 17 | 18 | dockerCommands += Cmd("ENV", "WATCH_PATHS=/schema") 19 | 20 | assemblyJarName := "graphql-gateway.jar" 21 | 22 | enablePlugins(JavaServerAppPackaging, DockerPlugin) -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.3 2 | -------------------------------------------------------------------------------- /project/coverage.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") 2 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.2") 3 | -------------------------------------------------------------------------------- /project/publishing.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.3") 2 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6") -------------------------------------------------------------------------------- /project/revolver.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1") -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loggers = ["akka.event.slf4j.Slf4jLogger"] 3 | loglevel = "DEBUG" 4 | logging-filter = "akka.event.slf4j.Slf4jLoggingFilter" 5 | } -------------------------------------------------------------------------------- /src/main/resources/assets/graphiql.html: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | 32 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
Loading...
58 | 59 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /src/main/resources/assets/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | GraphQL Playground 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 58 | 59 | 443 |
444 | 477 |
Loading 478 | GraphQL Playground 479 |
480 |
481 | 482 | 540 |
541 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | %cyan(%d{HH:mm:ss.SSS}) %highlight(%-5level) %cyan(%logger{5}) - %msg%n 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | port = 8080 2 | port = ${?PORT} 3 | 4 | bindHost = 0.0.0.0 5 | bindHost = ${?BIND_HOST} 6 | 7 | graphiql = true 8 | graphiql = ${?GRAPHIQL} 9 | 10 | playground = true 11 | playground = ${?PLAYGROUND} 12 | 13 | // Available directives: http, graphql, faker, basic 14 | includeDirectivesStr = ${?INCLUDE_DIRECTIVES} 15 | excludeDirectivesStr = ${?EXCLUDE_DIRECTIVES} 16 | 17 | slowLog { 18 | enabled = true 19 | enabled = ${?SLOW_LOG_ENABLED} 20 | 21 | threshold = 10 seconds 22 | threshold = ${?SLOW_LOG_THRESHOLD} 23 | 24 | extension = false 25 | extension = ${?SLOW_LOG_EXTENSION} 26 | 27 | // Allows apollo-tracing when X-Apollo-Tracing header is provided 28 | apolloTracing = true 29 | apolloTracing = ${?SLOW_LOG_APOLLO_TRACING} 30 | } 31 | 32 | watch { 33 | enabled = true 34 | enabled = ${?WATCH_ENABLED} 35 | 36 | threshold = 50 millis 37 | threshold = ${?WATCH_THRESHOLD} 38 | 39 | paths = ["."] 40 | pathsStr = ${?WATCH_PATHS} 41 | 42 | glob = ["**/*.graphql"] 43 | globStr = ${?WATCH_GLOB} 44 | } 45 | 46 | limit { 47 | complexity = 10000 48 | complexity = ${?LIMIT_COMPLEXITY} 49 | 50 | maxDepth = 15 51 | maxDepth = ${?LIMIT_MAX_DEPTH} 52 | 53 | allowIntrospection = true 54 | allowIntrospection = ${?ALLOW_INTROSPECTION} 55 | } -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/AppConfig.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway 2 | 3 | import better.files.File 4 | import com.typesafe.config.Config 5 | import net.ceedubs.ficus.Ficus._ 6 | import net.ceedubs.ficus.readers.ArbitraryTypeReader._ 7 | 8 | import scala.concurrent.duration.FiniteDuration 9 | 10 | case class WatchConfig( 11 | enabled: Boolean, 12 | paths: Seq[String], 13 | pathsStr: Option[String], 14 | threshold: FiniteDuration, 15 | glob: Seq[String], 16 | globStr: Option[String] 17 | ) { 18 | lazy val allPaths = pathsStr.map(_.split("\\s*,\\s*").toSeq) getOrElse paths 19 | lazy val allFiles = allPaths.map(File(_)) 20 | lazy val allGlobs = globStr.map(_.split("\\s*,\\s*").toSeq) getOrElse glob 21 | } 22 | 23 | case class LimitConfig( 24 | complexity: Double, 25 | maxDepth: Int, 26 | allowIntrospection: Boolean) 27 | 28 | case class SlowLogConfig( 29 | enabled: Boolean, 30 | threshold: FiniteDuration, 31 | extension: Boolean, 32 | apolloTracing: Boolean) 33 | 34 | case class AppConfig( 35 | port: Int, 36 | bindHost: String, 37 | graphiql: Boolean, 38 | playground: Boolean, 39 | slowLog: SlowLogConfig, 40 | watch: WatchConfig, 41 | limit: LimitConfig, 42 | includeDirectives: Option[Seq[String]], 43 | includeDirectivesStr: Option[String], 44 | excludeDirectives: Option[Seq[String]], 45 | excludeDirectivesStr: Option[String] 46 | ) { 47 | lazy val allIncludeDirectives = includeDirectivesStr.map(_.split("\\s*,\\s*").toSeq) orElse includeDirectives 48 | lazy val allExcludeDirectives = excludeDirectivesStr.map(_.split("\\s*,\\s*").toSeq) orElse excludeDirectives 49 | 50 | def isEnabled(directivesName: String) = 51 | !allExcludeDirectives.exists(_.contains(directivesName)) && ( 52 | allIncludeDirectives.isEmpty || 53 | allIncludeDirectives.exists(_.contains(directivesName))) 54 | } 55 | 56 | object AppConfig { 57 | def load(config: Config): AppConfig = config.as[AppConfig] 58 | } 59 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/Main.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import sangria.gateway.http.GatewayServer 5 | 6 | object Main extends App { 7 | val config = AppConfig.load(ConfigFactory.load()) 8 | val server = new GatewayServer 9 | 10 | server.startup(config) 11 | 12 | Runtime.getRuntime.addShutdownHook(new Thread { 13 | override def run(): Unit = server.shutdown() 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/file/FileMonitor.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.file 2 | 3 | import java.nio.file._ 4 | 5 | import better.files._ 6 | import com.sun.nio.file.SensitivityWatchEventModifier 7 | 8 | import scala.concurrent.ExecutionContext 9 | import scala.util.Try 10 | import scala.util.control.NonFatal 11 | 12 | class FileMonitor(val root: File, maxDepth: Int) extends File.Monitor { 13 | protected[this] val service = root.newWatchService 14 | 15 | def this(root: File, recursive: Boolean = true) = this(root, if (recursive) Int.MaxValue else 0) 16 | 17 | /** 18 | * If watching non-directory, don't react to siblings 19 | * @param target 20 | * @return 21 | */ 22 | protected[this] def reactTo(target: File) = root.isDirectory || root.isSamePathAs(target) 23 | 24 | protected[this] def process(key: WatchKey) = { 25 | val path = key.watchable().asInstanceOf[Path] 26 | 27 | import scala.collection.JavaConverters._ 28 | key.pollEvents().asScala foreach { 29 | case event: WatchEvent[Path] @unchecked ⇒ 30 | val target: File = path.resolve(event.context()) 31 | if (reactTo(target)) { 32 | if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) { 33 | val depth = root.relativize(target).getNameCount 34 | watch(target, (maxDepth - depth) max 0) // auto-watch new files in a directory 35 | } 36 | onEvent(event.kind(), target, event.count()) 37 | } 38 | case event ⇒ if (reactTo(path)) onUnknownEvent(event) 39 | } 40 | key.reset() 41 | } 42 | 43 | protected[this] def watch(file: File, depth: Int): Unit = { 44 | def toWatch: Iterator[File] = if (file.isDirectory) { 45 | file.walk(depth).filter(f ⇒ f.isDirectory && f.exists) 46 | } else { 47 | when(file.exists)(file.parent).iterator // There is no way to watch a regular file; so watch its parent instead 48 | } 49 | 50 | try 51 | toWatch 52 | .foreach(f ⇒ 53 | Try[Unit](f.path.register(service, File.Events.all.toArray, 54 | // this is com.sun internal, but the service is useless on OSX without it 55 | SensitivityWatchEventModifier.HIGH)) 56 | .recover {case error ⇒ onException(error)}.get) 57 | catch { 58 | case NonFatal(e) ⇒ onException(e) 59 | } 60 | } 61 | 62 | @inline private def when[A](condition: Boolean)(f: => A): Option[A] = if (condition) Some(f) else None 63 | 64 | def start()(implicit executionContext: ExecutionContext) = { 65 | watch(root, maxDepth) 66 | executionContext.execute(new Runnable { 67 | override def run() = { 68 | try Iterator.continually(service.take()).foreach(process) catch { 69 | case _: ClosedWatchServiceException ⇒ // just ignore, the service is already closed! 70 | } 71 | } 72 | }) 73 | } 74 | 75 | def close() = service.close() 76 | 77 | // Although this class is abstract, we give provide implementations so user can choose to implement a subset of these 78 | def onCreate(file: File, count: Int) = {} 79 | def onModify(file: File, count: Int) = {} 80 | def onDelete(file: File, count: Int) = {} 81 | def onUnknownEvent(event: WatchEvent[_]) = {} 82 | def onException(exception: Throwable) = {} 83 | } 84 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/file/FileMonitorActor.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.file 2 | 3 | import java.nio.file.{NoSuchFileException, StandardWatchEventKinds} 4 | 5 | import akka.actor.{Actor, ActorRef, Cancellable, PoisonPill, Props} 6 | import akka.event.Logging 7 | import better.files._ 8 | import sangria.gateway.file.FileWatcher._ 9 | 10 | import scala.collection.mutable 11 | import scala.concurrent.duration.FiniteDuration 12 | 13 | class FileMonitorActor(paths: Seq[File], threshold: FiniteDuration, globs: Seq[String], cb: Vector[File] ⇒ Unit) extends Actor { 14 | import FileMonitorActor._ 15 | 16 | import context.dispatcher 17 | 18 | val log = Logging(context.system, this) 19 | var watchers: Seq[ActorRef] = _ 20 | val pendingFiles: mutable.HashSet[File] = mutable.HashSet[File]() 21 | var scheduled: Option[Cancellable] = None 22 | 23 | override def preStart(): Unit = { 24 | watchers = paths.map(_.newWatcher(recursive = true)) 25 | 26 | watchers.foreach { watcher ⇒ 27 | watcher ! when(events = StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE) { 28 | case (_, file) ⇒ self ! FileChange(file) 29 | } 30 | } 31 | } 32 | 33 | def receive = { 34 | case FileChange(file) ⇒ 35 | try { 36 | if (file.exists && !file.isDirectory && globs.exists(file.glob(_, includePath = false).nonEmpty)) { 37 | pendingFiles += file 38 | 39 | if (scheduled.isEmpty) 40 | scheduled = Some(context.system.scheduler.scheduleOnce(threshold, self, Threshold)) 41 | } 42 | } catch { 43 | case _: NoSuchFileException ⇒ // ignore, it's ok 44 | } 45 | 46 | case Threshold ⇒ 47 | val files = pendingFiles.toVector.sortBy(_.name) 48 | 49 | if (files.nonEmpty) 50 | cb(files) 51 | 52 | pendingFiles.clear() 53 | scheduled = None 54 | } 55 | } 56 | 57 | object FileMonitorActor { 58 | case class FileChange(file: File) 59 | case object Threshold 60 | 61 | def props(paths: Seq[File], threshold: FiniteDuration, globs: Seq[String], cb: Vector[File] ⇒ Unit) = 62 | Props(new FileMonitorActor(paths, threshold, globs, cb)) 63 | } -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/file/FileUtil.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.file 2 | 3 | import better.files.File 4 | 5 | object FileUtil { 6 | def loadFiles(files: Seq[File], globs: Seq[String]) = { 7 | val foundFiles = files.flatMap(file ⇒ globs.flatMap(glob ⇒ file.glob(glob, includePath = false))).toSet.toVector.sortBy((file: File) ⇒ file.name) 8 | 9 | foundFiles.filterNot(_.isDirectory).map(f ⇒ f → f.lines.mkString("\n")) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/file/FileWatcher.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.file 2 | 3 | import akka.actor._ 4 | import better.files.{Disposable, File, newMultiMap, repeat} 5 | import better.files._ 6 | 7 | import scala.collection.mutable 8 | 9 | /** 10 | * An actor that can watch a file or a directory 11 | * Instead of directly calling the constructor of this, call file.newWatcher to create the actor 12 | * 13 | * @param file watch this file (or directory) 14 | * @param maxDepth In case of directories, how much depth should we watch 15 | */ 16 | class FileWatcher(file: File, maxDepth: Int) extends Actor { 17 | import FileWatcher._ 18 | 19 | def this(file: File, recursive: Boolean = true) = this(file, if (recursive) Int.MaxValue else 0) 20 | 21 | protected[this] val callbacks = newMultiMap[Event, Callback] 22 | 23 | protected[this] val monitor: File.Monitor = new FileMonitor(file, maxDepth) { 24 | override def onEvent(event: Event, file: File, count: Int) = self ! Message.NewEvent(event, file, count) 25 | override def onException(exception: Throwable) = self ! Status.Failure(exception) 26 | } 27 | 28 | override def preStart() = monitor.start()(executionContext = context.dispatcher) 29 | 30 | override def receive = { 31 | case Message.NewEvent(event, target, count) if callbacks.contains(event) => callbacks(event).foreach(f => repeat(count)(f(event -> target))) 32 | case Message.RegisterCallback(events, callback) => events.foreach(event => callbacks.addBinding(event, callback)) 33 | case Message.RemoveCallback(event, callback) => callbacks.removeBinding(event, callback) 34 | } 35 | 36 | override def postStop() = monitor.stop() 37 | } 38 | 39 | object FileWatcher { 40 | import java.nio.file.{Path, WatchEvent} 41 | 42 | type Event = WatchEvent.Kind[Path] 43 | type Callback = PartialFunction[(Event, File), Unit] 44 | 45 | sealed trait Message 46 | object Message { 47 | case class NewEvent(event: Event, file: File, count: Int) extends Message 48 | case class RegisterCallback(events: Traversable[Event], callback: Callback) extends Message 49 | case class RemoveCallback(event: Event, callback: Callback) extends Message 50 | } 51 | 52 | implicit val disposeActorSystem: Disposable[ActorSystem] = 53 | Disposable(_.terminate()) 54 | 55 | implicit class FileWatcherOps(file: File) { 56 | def watcherProps(recursive: Boolean): Props = 57 | Props(new FileWatcher(file, recursive)) 58 | 59 | def newWatcher(recursive: Boolean = true)(implicit ctx: ActorRefFactory): ActorRef = 60 | ctx.actorOf(watcherProps(recursive)) 61 | } 62 | 63 | def when(events: Event*)(callback: Callback): Message = 64 | Message.RegisterCallback(events, callback) 65 | 66 | def on(event: Event)(callback: File => Unit): Message = 67 | when(event) { case (`event`, file) => callback(file) } 68 | 69 | def stop(event: Event, callback: Callback): Message = 70 | Message.RemoveCallback(event, callback) 71 | 72 | private def newMultiMap[A, B]: mutable.MultiMap[A, B] = new mutable.HashMap[A, mutable.Set[B]] with mutable.MultiMap[A, B] 73 | @inline private def repeat[U](n: Int)(f: => U): Unit = (1 to n).foreach(_ ⇒ f) 74 | } 75 | 76 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/http/GatewayServer.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.http 2 | 3 | import language.postfixOps 4 | import akka.actor.ActorSystem 5 | import akka.http.scaladsl.Http 6 | import akka.stream.ActorMaterializer 7 | import play.api.libs.ws.ahc.StandaloneAhcWSClient 8 | import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClient 9 | import sangria.gateway.AppConfig 10 | import sangria.gateway.http.client.PlayHttpClient 11 | import sangria.gateway.schema.materializer.GatewayMaterializer 12 | import sangria.gateway.schema.materializer.directive._ 13 | import sangria.gateway.schema.{ReloadableSchemaProvider, StaticSchemaProvider} 14 | import sangria.gateway.util.Logging 15 | 16 | import scala.util.{Failure, Success} 17 | import scala.util.control.NonFatal 18 | 19 | class GatewayServer extends Logging { 20 | implicit val system: ActorSystem = ActorSystem("sangria-server") 21 | implicit val materializer: ActorMaterializer = ActorMaterializer() 22 | 23 | import system.dispatcher 24 | 25 | //val client = new AkkaHttpClient 26 | val client = new PlayHttpClient(new StandaloneAhcWSClient(new DefaultAsyncHttpClient)) 27 | 28 | val directiveProviders = Map( 29 | "http" → new HttpDirectiveProvider(client), 30 | "graphql" → new GraphQLDirectiveProvider, 31 | "faker" → new FakerDirectiveProvider, 32 | "basic" → new BasicDirectiveProvider) 33 | 34 | def startup(config: AppConfig) = 35 | try { 36 | val gatewayMaterializer = new GatewayMaterializer(filterDirectives(config, directiveProviders)) 37 | 38 | val schemaProvider = 39 | if (config.watch.enabled) 40 | new ReloadableSchemaProvider(config, client, gatewayMaterializer) 41 | else 42 | new StaticSchemaProvider(config, client, gatewayMaterializer) 43 | 44 | schemaProvider.schemaInfo // trigger initial schema load at startup 45 | 46 | val routing = new GraphQLRouting(config, schemaProvider) 47 | 48 | Http().bindAndHandle(routing.route, config.bindHost, config.port).andThen { 49 | case Success(_) ⇒ 50 | logger.info(s"Server started on ${config.bindHost}:${config.port}") 51 | 52 | if (config.watch.enabled) 53 | logger.info(s"Watching files at following path: ${config.watch.allFiles.mkString(", ")}. Looking for files: ${config.watch.allGlobs.mkString(", ")}.") 54 | 55 | case Failure(_) ⇒ 56 | shutdown() 57 | } 58 | } catch { 59 | case NonFatal(error) ⇒ 60 | logger.error("Error during server startup", error) 61 | 62 | shutdown() 63 | } 64 | 65 | def shutdown(): Unit = { 66 | logger.info("Shutting down server") 67 | system.terminate() 68 | } 69 | 70 | private def filterDirectives(config: AppConfig, providers: Map[String, DirectiveProvider]) = { 71 | val includes = config.allIncludeDirectives.fold(Set.empty[String])(_.toSet) 72 | val excludes = config.allExcludeDirectives.fold(Set.empty[String])(_.toSet) 73 | val initial = providers.toVector 74 | 75 | val withIncludes = 76 | if (config.allIncludeDirectives.nonEmpty) 77 | initial.filter(dp ⇒ includes contains dp._1) 78 | else 79 | initial 80 | 81 | val withExcludes = 82 | if (config.allExcludeDirectives.nonEmpty) 83 | withIncludes.filterNot(dp ⇒ excludes contains dp._1) 84 | else 85 | withIncludes 86 | 87 | withExcludes.map(_._2) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/http/GraphQLRequestUnmarshaller.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.http 2 | 3 | import java.nio.charset.Charset 4 | 5 | import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller} 6 | import akka.http.scaladsl.model._ 7 | import akka.http.scaladsl.model.headers.Accept 8 | import akka.http.scaladsl.server.Directive0 9 | import akka.http.scaladsl.server.Directives._ 10 | import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} 11 | import akka.util.ByteString 12 | import sangria.ast.Document 13 | import sangria.parser.QueryParser 14 | import sangria.renderer.{QueryRenderer, QueryRendererConfig} 15 | 16 | import scala.collection.immutable.Seq 17 | 18 | object GraphQLRequestUnmarshaller { 19 | val `application/graphql` = MediaType.applicationWithFixedCharset("graphql", HttpCharsets.`UTF-8`, "graphql") 20 | 21 | def explicitlyAccepts(mediaType: MediaType): Directive0 = 22 | headerValuePF { 23 | case Accept(ranges) if ranges.exists(range ⇒ !range.isWildcard && range.matches(mediaType)) ⇒ ranges 24 | }.flatMap(_ ⇒ pass) 25 | 26 | def includeIf(include: Boolean): Directive0 = 27 | if (include) pass 28 | else reject 29 | 30 | def unmarshallerContentTypes: Seq[ContentTypeRange] = 31 | mediaTypes.map(ContentTypeRange.apply) 32 | 33 | def mediaTypes: Seq[MediaType.WithFixedCharset] = 34 | List(`application/graphql`) 35 | 36 | implicit final def documentMarshaller(implicit config: QueryRendererConfig = QueryRenderer.Compact): ToEntityMarshaller[Document] = 37 | Marshaller.oneOf(mediaTypes: _*) { mediaType ⇒ 38 | Marshaller.withFixedContentType(ContentType(mediaType)) { json ⇒ 39 | HttpEntity(mediaType, QueryRenderer.render(json, config)) 40 | } 41 | } 42 | 43 | /** 44 | * HTTP entity => `Json` 45 | * 46 | * @return unmarshaller for `Json` 47 | */ 48 | implicit final val documentUnmarshaller: FromEntityUnmarshaller[Document] = 49 | Unmarshaller.byteStringUnmarshaller 50 | .forContentTypes(unmarshallerContentTypes: _*) 51 | .map { 52 | case ByteString.empty ⇒ throw Unmarshaller.NoContentException 53 | case data ⇒ 54 | import sangria.parser.DeliveryScheme.Throw 55 | 56 | QueryParser.parse(data.decodeString(Charset.forName("UTF-8"))) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/http/GraphQLRouting.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.http 2 | 3 | import akka.http.scaladsl.model.StatusCodes._ 4 | import akka.http.scaladsl.server.Directives._ 5 | import akka.http.scaladsl.model.headers._ 6 | import akka.http.scaladsl.server.{ExceptionHandler => _, _} 7 | import de.heikoseeberger.akkahttpcirce.ErrorAccumulatingCirceSupport.{jsonMarshaller, jsonUnmarshaller} 8 | import sangria.gateway.http.GraphQLRequestUnmarshaller.{explicitlyAccepts, _} 9 | import akka.http.scaladsl.marshalling.{ToResponseMarshallable => TRM} 10 | import akka.http.scaladsl.model.HttpEntity 11 | import akka.http.scaladsl.model.MediaTypes.`text/html` 12 | import de.heikoseeberger.akkasse.scaladsl.model._ 13 | import de.heikoseeberger.akkasse.scaladsl.marshalling.EventStreamMarshalling._ 14 | import io.circe._ 15 | import io.circe.optics.JsonPath._ 16 | import io.circe.parser._ 17 | import sangria.ast.Document 18 | import sangria.execution._ 19 | import sangria.gateway.AppConfig 20 | import sangria.gateway.schema.SchemaProvider 21 | import sangria.gateway.schema.materializer.GatewayContext 22 | import sangria.gateway.util.Logging 23 | import sangria.parser.{QueryParser, SyntaxError} 24 | import sangria.marshalling.circe._ 25 | import sangria.slowlog.SlowLog 26 | 27 | import scala.concurrent.{ExecutionContext, Future} 28 | import scala.util.{Failure, Success} 29 | import scala.util.control.NonFatal 30 | 31 | class GraphQLRouting[Val](config: AppConfig, schemaProvider: SchemaProvider[GatewayContext, Val])(implicit ec: ExecutionContext) extends Logging { 32 | val route: Route = 33 | optionalHeaderValueByName("X-Apollo-Tracing") { tracing ⇒ 34 | path("graphql") { 35 | get { 36 | (includeIf(config.graphiql || config.playground) & explicitlyAccepts(`text/html`)) { 37 | if (config.playground) 38 | getFromResource("assets/playground.html") 39 | else 40 | getFromResource("assets/graphiql.html") 41 | } ~ 42 | (extractRequestContext & parameters('query, 'operationName.?, 'variables.?)) { (request, query, operationName, variables) ⇒ 43 | QueryParser.parse(query) match { 44 | case Success(ast) ⇒ 45 | variables.map(parse) match { 46 | case Some(Left(error)) ⇒ complete(BadRequest, formatError(error)) 47 | case Some(Right(json)) ⇒ executeGraphQL(ast, operationName, json, tracing.isDefined, extractHeaders(request)) 48 | case None ⇒ executeGraphQL(ast, operationName, Json.obj(), tracing.isDefined, extractHeaders(request)) 49 | } 50 | case Failure(error) ⇒ complete(BadRequest, formatError(error)) 51 | } 52 | } 53 | } ~ 54 | post { 55 | (extractRequestContext & parameters('query.?, 'operationName.?, 'variables.?)) { (request, queryParam, operationNameParam, variablesParam) ⇒ 56 | entity(as[Json]) { body ⇒ 57 | val query = queryParam orElse root.query.string.getOption(body) 58 | val operationName = operationNameParam orElse root.operationName.string.getOption(body) 59 | val variablesStr = variablesParam orElse root.variables.string.getOption(body) 60 | 61 | query.map(QueryParser.parse(_)) match { 62 | case Some(Success(ast)) ⇒ 63 | variablesStr.map(parse) match { 64 | case Some(Left(error)) ⇒ complete(BadRequest, formatError(error)) 65 | case Some(Right(json)) ⇒ executeGraphQL(ast, operationName, json, tracing.isDefined, extractHeaders(request)) 66 | case None ⇒ executeGraphQL(ast, operationName, root.variables.json.getOption(body) getOrElse Json.obj(), tracing.isDefined, extractHeaders(request)) 67 | } 68 | case Some(Failure(error)) ⇒ complete(BadRequest, formatError(error)) 69 | case None ⇒ complete(BadRequest, formatError("No query to execute")) 70 | } 71 | } ~ 72 | entity(as[Document]) { document ⇒ 73 | variablesParam.map(parse) match { 74 | case Some(Left(error)) ⇒ complete(BadRequest, formatError(error)) 75 | case Some(Right(json)) ⇒ executeGraphQL(document, operationNameParam, json, tracing.isDefined, extractHeaders(request)) 76 | case None ⇒ executeGraphQL(document, operationNameParam, Json.obj(), tracing.isDefined, extractHeaders(request)) 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } ~ 83 | (get & path("schema-updates")) { 84 | schemaProvider.schemaChanges match { 85 | case Some(source) ⇒ 86 | complete(source.filter(identity).map(_ ⇒ ServerSentEvent("changed"))) 87 | case None ⇒ 88 | complete(200 → "Schema reloading is disabled") 89 | } 90 | } ~ 91 | (get & path("schema.json")) { 92 | complete(schemaProvider.schemaInfo.map { 93 | case Some(info) ⇒ TRM(OK → info.schemaIntrospection) 94 | case None ⇒ noSchema 95 | }) 96 | } ~ 97 | (get & path("schema.graphql")) { 98 | complete(schemaProvider.schemaInfo.map { 99 | case Some(info) ⇒ TRM(OK → HttpEntity(`application/graphql`, info.schemaRendered)) 100 | case None ⇒ noSchema 101 | }) 102 | } ~ 103 | (get & pathEndOrSingleSlash) { 104 | redirect("/graphql", PermanentRedirect) 105 | } 106 | 107 | private val reducers = { 108 | val complexityRejector = QueryReducer.rejectComplexQueries(config.limit.complexity, (complexity: Double, _: Any) ⇒ 109 | TooComplexQueryError(s"Query complexity is $complexity but max allowed complexity is ${config.limit.complexity}. Please reduce the number of the fields in the query.")) 110 | 111 | val depthRejector = QueryReducer.rejectMaxDepth(config.limit.maxDepth) 112 | 113 | val baseReducers = 114 | if (config.limit.allowIntrospection) 115 | Nil 116 | else 117 | QueryReducer.rejectIntrospection(includeTypeName = false) :: Nil 118 | 119 | baseReducers ++ List(complexityRejector, depthRejector) 120 | } 121 | 122 | private val middleware = 123 | if (config.slowLog.enabled) 124 | SlowLog(logger.underlying, config.slowLog.threshold, config.slowLog.extension) :: Nil 125 | else 126 | Nil 127 | 128 | def extractHeaders(request: RequestContext) = 129 | request.request.headers.map(h ⇒ h.name() → h.value()) 130 | 131 | def executeGraphQL(query: Document, operationName: Option[String], variables: Json, tracing: Boolean, originalHeaders: Seq[(String, String)]) = 132 | complete(schemaProvider.schemaInfo.flatMap { 133 | case Some(schemaInfo) ⇒ 134 | val actualVariables = if (variables.isNull) Json.obj() else variables 135 | 136 | Executor.execute(schemaInfo.schema, query, schemaInfo.ctx.copy(operationName = operationName, queryVars = actualVariables, originalHeaders = originalHeaders), 137 | root = schemaInfo.value, 138 | variables = actualVariables, 139 | operationName = operationName, 140 | queryReducers = reducers.asInstanceOf[List[QueryReducer[GatewayContext, _]]], 141 | middleware = middleware ++ schemaInfo.middleware ++ (if (tracing && config.slowLog.apolloTracing) SlowLog.apolloTracing :: Nil else Nil), 142 | exceptionHandler = exceptionHandler, 143 | deferredResolver = schemaInfo.deferredResolver) 144 | .map(res ⇒ TRM(OK → res)) 145 | .recover { 146 | case error: QueryAnalysisError ⇒ TRM(BadRequest → error.resolveError) 147 | case error: ErrorWithResolver ⇒ TRM(InternalServerError → error.resolveError) 148 | } 149 | case None ⇒ Future.successful(noSchema) 150 | }) 151 | 152 | def formatError(error: Throwable): Json = error match { 153 | case syntaxError: SyntaxError ⇒ 154 | Json.obj("errors" → Json.arr( 155 | Json.obj( 156 | "message" → Json.fromString(syntaxError.getMessage), 157 | "locations" → Json.arr(Json.obj( 158 | "line" → Json.fromBigInt(syntaxError.originalError.position.line), 159 | "column" → Json.fromBigInt(syntaxError.originalError.position.column)))))) 160 | case NonFatal(e) ⇒ 161 | formatError(e.getMessage) 162 | case e ⇒ 163 | throw e 164 | } 165 | 166 | def formatError(message: String): Json = 167 | Json.obj("errors" → Json.arr(Json.obj("message" → Json.fromString(message)))) 168 | 169 | def noSchema = TRM(InternalServerError → "No schema defined") 170 | 171 | val exceptionHandler = ExceptionHandler { 172 | case (m, error: TooComplexQueryError) ⇒ HandledException(error.getMessage) 173 | case (m, QueryReducingError(error: MaxQueryDepthReachedError, _)) ⇒ HandledException(error.getMessage) 174 | case (m, NonFatal(error)) ⇒ 175 | logger.error("Error during GraphQL query execution", error) 176 | HandledException(error.getMessage) 177 | } 178 | 179 | case class TooComplexQueryError(message: String) extends Exception(message) 180 | } 181 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/http/client/AkkaHttpClient.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.http.client 2 | 3 | import akka.actor.ActorSystem 4 | import akka.http.scaladsl.Http 5 | import akka.http.scaladsl.model.HttpHeader.ParsingResult 6 | import akka.http.scaladsl.model._ 7 | import akka.http.scaladsl.model.Uri.Query 8 | import akka.http.scaladsl.model.headers.Location 9 | import akka.http.scaladsl.unmarshalling.Unmarshal 10 | import akka.stream.Materializer 11 | import akka.util.ByteString 12 | import sangria.gateway.util.Logging 13 | 14 | import scala.concurrent.{ExecutionContext, Future} 15 | 16 | class AkkaHttpClient(implicit system: ActorSystem, mat: Materializer, ec: ExecutionContext) extends HttpClient with Logging { 17 | import AkkaHttpClient._ 18 | import HttpClient._ 19 | 20 | override def request(method: Method.Value, url: String, queryParams: Seq[(String, String)] = Seq.empty, headers: Seq[(String, String)] = Seq.empty, body: Option[(String, String)] = None) = { 21 | val m = mapMethod(method) 22 | val query = Query(queryParams: _*) 23 | val hs = headers.map(header) 24 | val uri = Uri(url).withQuery(query) 25 | val entity = body.fold(HttpEntity.Empty){case (tpe, content) ⇒ HttpEntity(contentType(tpe), ByteString(content))} 26 | val request = HttpRequest(m, uri, hs.toVector, entity) 27 | val client = Http().singleRequest(_: HttpRequest) 28 | val richClient = RichHttpClient.httpClientWithRedirect(client) 29 | 30 | logger.debug(s"Http request: ${m.value} $url") 31 | 32 | richClient(request).map(AkkaHttpResponse(m, url, _)) 33 | } 34 | 35 | override def oauthClientCredentials(url: String, clientId: String, clientSecret: String, scopes: Seq[String]): Future[HttpResponse] = 36 | throw new IllegalStateException("Not yet implemented, please use play implementation.") 37 | 38 | private def contentType(str: String) = ContentType.parse(str).fold( 39 | errors ⇒ throw ClientError(s"Invalid content type '$str'", errors.map(_.detail)), 40 | identity) 41 | 42 | private def header(nameValue: (String, String)) = HttpHeader.parse(nameValue._1, nameValue._2) match { 43 | case ParsingResult.Ok(_, errors) if errors.nonEmpty ⇒ throw ClientError(s"Invalid header '${nameValue._1}'", errors.map(_.detail)) 44 | case ParsingResult.Error(error) ⇒ throw ClientError(s"Invalid header '${nameValue._1}'", Seq(error.detail)) 45 | case ParsingResult.Ok(h, _) ⇒ h 46 | } 47 | 48 | def mapMethod(method: Method.Value) = method match { 49 | case Method.Get ⇒ HttpMethods.GET 50 | case Method.Post ⇒ HttpMethods.POST 51 | } 52 | 53 | object RichHttpClient { 54 | import akka.http.scaladsl.model.HttpResponse 55 | type HttpClient = HttpRequest ⇒ Future[HttpResponse] 56 | 57 | def redirectOrResult(client: HttpClient)(response: HttpResponse): Future[HttpResponse] = 58 | response.status match { 59 | case StatusCodes.Found | StatusCodes.MovedPermanently | StatusCodes.SeeOther ⇒ 60 | val newUri = response.header[Location].get.uri 61 | // Always make sure you consume the response entity streams (of type Source[ByteString,Unit]) by for example connecting it to a Sink (for example response.discardEntityBytes() if you don’t care about the response entity), since otherwise Akka HTTP (and the underlying Streams infrastructure) will understand the lack of entity consumption as a back-pressure signal and stop reading from the underlying TCP connection! 62 | response.discardEntityBytes() 63 | 64 | logger.debug(s"Http redirect: ${HttpMethods.GET.value} $newUri") 65 | 66 | client(HttpRequest(method = HttpMethods.GET, uri = newUri)) 67 | 68 | case _ ⇒ Future.successful(response) 69 | } 70 | 71 | def httpClientWithRedirect(client: HttpClient): HttpClient = { 72 | lazy val redirectingClient: HttpClient = 73 | req ⇒ client(req).flatMap(redirectOrResult(redirectingClient)) // recurse to support multiple redirects 74 | 75 | redirectingClient 76 | } 77 | } 78 | 79 | case class ClientError(message: String, errors: Seq[String]) extends Exception(message + ":\n" + errors.map(" * " + _).mkString("\n")) 80 | } 81 | 82 | object AkkaHttpClient { 83 | case class AkkaHttpResponse(method: HttpMethod, url: String, response: HttpResponse)(implicit mat: Materializer) extends HttpClient.HttpResponse { 84 | def asString = Unmarshal(response).to[String] 85 | def statusCode = response.status.intValue() 86 | def isSuccessful = response.status.isSuccess() 87 | def debugInfo = s"${method.value} $url" 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/http/client/HttpClient.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.http.client 2 | 3 | import scala.concurrent.Future 4 | 5 | trait HttpClient { 6 | import HttpClient._ 7 | 8 | def request( 9 | method: Method.Value, 10 | url: String, 11 | queryParams: Seq[(String, String)] = Seq.empty, 12 | headers: Seq[(String, String)] = Seq.empty, 13 | body: Option[(String, String)] = None): Future[HttpResponse] 14 | 15 | def oauthClientCredentials( 16 | url: String, 17 | clientId: String, 18 | clientSecret: String, 19 | scopes: Seq[String]): Future[HttpResponse] 20 | } 21 | 22 | object HttpClient { 23 | object Method extends Enumeration { 24 | val Get, Post = Value 25 | } 26 | 27 | trait HttpResponse { 28 | def statusCode: Int 29 | def isSuccessful: Boolean 30 | def asString: Future[String] 31 | def debugInfo: String 32 | } 33 | } -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/http/client/PlayHttpClient.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.http.client 2 | 3 | import akka.util.ByteString 4 | import io.circe.parser._ 5 | import play.api.libs.ws.{BodyWritable, InMemoryBody, StandaloneWSClient, WSAuthScheme} 6 | import sangria.gateway.http.client.HttpClient.Method 7 | import sangria.gateway.util.Logging 8 | 9 | import scala.concurrent.duration._ 10 | import scala.concurrent.{ExecutionContext, Future} 11 | 12 | class PlayHttpClient(ws: StandaloneWSClient)(implicit ec: ExecutionContext) extends play.api.libs.ws.DefaultBodyWritables with HttpClient with Logging { 13 | private[this] val json = "application/json" 14 | 15 | override def request( 16 | method: Method.Value, url: String, 17 | queryParams: Seq[(String, String)], headers: Seq[(String, String)], 18 | body: Option[(String, String)] 19 | ) = { 20 | val m = mapMethod(method) 21 | val baseRequest = ws.url(url).withRequestTimeout(10.minutes).withMethod(m) 22 | val withParams = baseRequest.withQueryStringParameters(queryParams: _*).withHttpHeaders(headers: _*) 23 | val withBody = body match { 24 | case Some((t, content)) if t == json ⇒ withParams.withBody(parse(content).right.get)(BodyWritable(s ⇒ InMemoryBody(ByteString(s.spaces2)), t)) 25 | case Some((t, _)) ⇒ throw new IllegalStateException(s"Unhandled body type [$t].") 26 | case None ⇒ withParams 27 | } 28 | val finalRequest = withBody 29 | 30 | logger.debug(s"Http request: $m $url") 31 | 32 | finalRequest.execute().map(rsp ⇒ new HttpClient.HttpResponse { 33 | override def statusCode = rsp.status 34 | override def isSuccessful = rsp.status >= 200 && rsp.status < 300 35 | override def asString = Future.successful(rsp.body) 36 | override def debugInfo = s"$method $url" 37 | }) 38 | } 39 | 40 | override def oauthClientCredentials(url: String, clientId: String, clientSecret: String, scopes: Seq[String]): Future[HttpClient.HttpResponse] = { 41 | val request = 42 | ws.url(url) 43 | .withMethod("POST") 44 | .withAuth(clientId, clientSecret, WSAuthScheme.BASIC) 45 | .withBody(Map("grant_type" → Seq("client_credentials"), "scope" → scopes)) 46 | 47 | logger.debug(s"HTTP OAuth client credentials request: $url") 48 | 49 | request.execute().map(rsp ⇒ new HttpClient.HttpResponse { 50 | override def statusCode = rsp.status 51 | override def isSuccessful = rsp.status >= 200 && rsp.status < 300 52 | override def asString = Future.successful(rsp.body) 53 | override def debugInfo = s"POST $url" 54 | }) 55 | } 56 | 57 | def mapMethod(method: Method.Value) = method match { 58 | case Method.Get ⇒ "GET" 59 | case Method.Post ⇒ "POST" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/json/CirceJsonPath.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.json 2 | 3 | import com.jayway.jsonpath.{Configuration, JsonPath} 4 | import io.circe.Json 5 | 6 | object CirceJsonPath { 7 | private lazy val config = 8 | Configuration.builder() 9 | .jsonProvider(new CirceJsonProvider) 10 | .mappingProvider(new CirceMappingProvider) 11 | .build() 12 | 13 | def query(json: Json, jsonPath: String): Json = 14 | JsonPathValueWrapper.toJson(JsonPath.using(config).parse(json).read(jsonPath)) 15 | } 16 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/json/CirceJsonProvider.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.json 2 | 3 | import java.io.InputStream 4 | 5 | import com.jayway.jsonpath.{InvalidJsonException, JsonPathException} 6 | import com.jayway.jsonpath.spi.json.JsonProvider 7 | import io.circe.{Json, JsonObject} 8 | 9 | import scala.collection.JavaConverters._ 10 | 11 | import scala.io.Source 12 | 13 | class CirceJsonProvider extends JsonProvider { 14 | override def createArray(): AnyRef = JsonPathValueWrapper.emptyArray 15 | 16 | override def setArrayIndex(array: Any, idx: Int, newValue: Any): Unit = 17 | array match { 18 | case arr: java.util.ArrayList[Any] @unchecked ⇒ if (idx == arr.size) arr.add(newValue) else arr.set(idx, newValue) 19 | case _ ⇒ error("setArrayIndex is only available on new objects") 20 | } 21 | 22 | override def length(obj: Any): Int = { 23 | obj match { 24 | case json: Json if json.isArray ⇒ 25 | json.asArray.get.size 26 | case json: Json if json.isObject ⇒ 27 | json.asObject.get.size 28 | case json: Json if json.isString ⇒ 29 | json.asString.get.length 30 | case arr: java.util.ArrayList[_] ⇒ 31 | arr.size 32 | case obj: java.util.LinkedHashMap[_, _] ⇒ 33 | obj.size 34 | case s: String ⇒ 35 | s.length 36 | case _ ⇒ throw new JsonPathException(s"Length operation cannot be applied to ${if (obj != null) obj.getClass.getName else "null"}") 37 | } 38 | } 39 | 40 | override def getArrayIndex(obj: Any, idx: Int): AnyRef = obj match { 41 | case arr: java.util.ArrayList[AnyRef @unchecked] ⇒ arr.get(idx) 42 | case json: Json if json.isArray ⇒ json.asArray.get(idx) 43 | case o ⇒ notJson(o) 44 | } 45 | 46 | override def getArrayIndex(obj: Any, idx: Int, unwrap: Boolean): AnyRef = 47 | getArrayIndex(obj, idx) 48 | 49 | override def createMap(): AnyRef = JsonPathValueWrapper.emptyMap 50 | override def setProperty(obj: Any, key: Any, value: Any): Unit = obj match { 51 | case obj: java.util.LinkedHashMap[String @unchecked, Any @unchecked] ⇒ obj.put(key.asInstanceOf[String], value) 52 | case _ ⇒ error("setProperty is only available on new objects") 53 | } 54 | override def getPropertyKeys(obj: Any) = 55 | obj match { 56 | case obj: java.util.LinkedHashMap[String, Any] @unchecked ⇒ obj.keySet 57 | case obj: Json if obj.isObject ⇒ obj.asObject.get.keys.asJavaCollection 58 | case o ⇒ Vector.empty.asJavaCollection 59 | } 60 | 61 | override def removeProperty(obj: Any, key: Any): Unit = 62 | obj match { 63 | case obj: java.util.LinkedHashMap[_, _] ⇒ obj.remove(key) 64 | case _ ⇒ error("removeProperty is only available on new objects") 65 | } 66 | 67 | override def getMapValue(obj: Any, key: String): AnyRef = 68 | obj match { 69 | case obj: java.util.LinkedHashMap[String, AnyRef] @unchecked if obj.containsKey(key) ⇒ obj.get(key) 70 | case json: Json if json.isObject && json.asObject.get.contains(key) ⇒ json.asObject.get(key).get 71 | case _ ⇒ JsonProvider.UNDEFINED 72 | } 73 | 74 | override def toIterable(obj: Any) = 75 | if (isArray(obj)) obj.asInstanceOf[Json].asArray.get.asJava 76 | else error(s"Cannot iterate over ${if (obj != null) obj.getClass.getName else "null"}") 77 | 78 | override def unwrap(obj: Any): AnyRef = 79 | obj match { 80 | case json: Json ⇒ 81 | json.fold( 82 | jsonNull = null, 83 | jsonBoolean = b ⇒ b: java.lang.Boolean, 84 | jsonNumber = _.toBigDecimal.get, 85 | jsonString = identity, 86 | jsonArray = JsonPathValueWrapper.array(_), 87 | jsonObject = JsonPathValueWrapper.map(_)) 88 | 89 | case obj ⇒ obj.asInstanceOf[AnyRef] 90 | } 91 | 92 | override def isMap(obj: Any): Boolean = obj match { 93 | case obj: java.util.HashMap[_, _] ⇒ true 94 | case obj: Json if obj.isObject ⇒ true 95 | case _ ⇒ false 96 | } 97 | 98 | override def isArray(obj: Any): Boolean = obj match { 99 | case obj: java.util.ArrayList[_] ⇒ true 100 | case obj: Json if obj.isArray ⇒ true 101 | case _ ⇒ false 102 | } 103 | 104 | override def toJson(obj: Any): String = 105 | obj match { 106 | case json: Json ⇒ json.spaces2 107 | case obj ⇒ JsonPathValueWrapper.toJson(obj).spaces2 108 | } 109 | 110 | override def parse(json: String): AnyRef = 111 | io.circe.parser.parse(json).fold(e ⇒ throw new InvalidJsonException(e, json), identity) 112 | 113 | override def parse(jsonStream: InputStream, charset: String): AnyRef = { 114 | val json = Source.fromInputStream(jsonStream, charset).getLines.mkString("\n") 115 | 116 | io.circe.parser.parse(json).fold(e ⇒ throw new InvalidJsonException(e, json), identity) 117 | } 118 | 119 | private def notJson(obj: Any) = error("Not a JSON value") 120 | private def error(message: String) = throw new JsonPathException(message) 121 | } 122 | 123 | object JsonPathValueWrapper { 124 | def emptyMap: java.util.LinkedHashMap[String, Any] = 125 | new java.util.LinkedHashMap[String, Any] 126 | 127 | def map(obj: JsonObject): java.util.LinkedHashMap[String, Any] = { 128 | val values = new java.util.LinkedHashMap[String, Any](obj.size) 129 | 130 | obj.keys.foreach { key ⇒ 131 | values.put(key, obj(key).get) 132 | } 133 | 134 | values 135 | } 136 | 137 | def emptyArray = new java.util.ArrayList[Any] 138 | 139 | def array(obj: Vector[Any]): java.util.ArrayList[Any] = { 140 | val values = new java.util.ArrayList[Any](obj.size) 141 | 142 | obj.foreach { key ⇒ 143 | values.add(key) 144 | } 145 | 146 | values 147 | } 148 | 149 | def toJson(obj: Any): Json = obj match { 150 | case arr: java.util.ArrayList[_] ⇒ 151 | Json.arr(arr.asScala.map(toJson): _*) 152 | case obj: java.util.LinkedHashMap[String, Any] @unchecked ⇒ 153 | Json.obj(obj.asScala.toSeq.map{case (k, v) ⇒ k → toJson(v)}: _*) 154 | case json: Json ⇒ 155 | json 156 | case v: String ⇒ 157 | Json.fromString(v) 158 | case v: Boolean ⇒ 159 | Json.fromBoolean(v) 160 | case v: Int ⇒ 161 | Json.fromInt(v) 162 | case v: Float ⇒ 163 | Json.fromFloat(v).get 164 | case v: Double ⇒ 165 | Json.fromDouble(v).get 166 | case v: BigInt ⇒ 167 | Json.fromBigInt(v) 168 | case v: BigDecimal ⇒ 169 | Json.fromBigDecimal(v) 170 | case null ⇒ 171 | Json.Null 172 | case v ⇒ 173 | throw new JsonPathException("Unsupported value: " + v) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/json/CirceMappingProvider.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.json 2 | 3 | import com.jayway.jsonpath.{Configuration, TypeRef} 4 | import com.jayway.jsonpath.spi.mapper.MappingProvider 5 | 6 | class CirceMappingProvider extends MappingProvider { 7 | override def map[T](source: Any, targetType: Class[T], configuration: Configuration): T = 8 | throw new UnsupportedOperationException("Circe JSON provider does not support mapping!") 9 | 10 | override def map[T](source: Any, targetType: TypeRef[T], configuration: Configuration): T = 11 | throw new UnsupportedOperationException("Circe JSON provider does not support TypeRef mapping!") 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/schema/CustomScalars.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.schema 2 | 3 | import java.time.format.DateTimeFormatter 4 | import java.time.{Instant, OffsetDateTime, ZoneOffset, ZonedDateTime} 5 | 6 | import sangria.schema._ 7 | import sangria.ast 8 | import sangria.validation.ValueCoercionViolation 9 | 10 | import scala.util.{Failure, Success, Try} 11 | 12 | object CustomScalars { 13 | implicit val DateTimeType = ScalarType[ZonedDateTime]("DateTime", 14 | description = Some("DateTime is a scalar value that represents an ISO8601 formatted date and time."), 15 | coerceOutput = (date, _) ⇒ DateTimeFormatter.ISO_INSTANT.format(date), 16 | coerceUserInput = { 17 | case s: String ⇒ parseDateTime(s) match { 18 | case Success(date) ⇒ Right(date) 19 | case Failure(_) ⇒ Left(DateCoercionViolation) 20 | } 21 | case _ ⇒ Left(DateCoercionViolation) 22 | }, 23 | coerceInput = { 24 | case ast.StringValue(s, _, _, _, _) ⇒ parseDateTime(s) match { 25 | case Success(date) ⇒ Right(date) 26 | case Failure(_) ⇒ Left(DateCoercionViolation) 27 | } 28 | case _ ⇒ Left(DateCoercionViolation) 29 | }) 30 | 31 | def parseDateTime(s: String) = Try(DateTimeFormatter.ISO_ZONED_DATE_TIME.parse(s).asInstanceOf[ZonedDateTime]) 32 | 33 | case object DateCoercionViolation extends ValueCoercionViolation("Date value expected") 34 | } 35 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/schema/ReloadableSchemaProvider.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.schema 2 | 3 | import java.util.concurrent.atomic.AtomicReference 4 | 5 | import akka.actor.ActorSystem 6 | import akka.stream.{Materializer, OverflowStrategy} 7 | import akka.stream.scaladsl.{BroadcastHub, Keep, RunnableGraph, Source} 8 | import better.files.File 9 | import sangria.gateway.AppConfig 10 | import sangria.gateway.file.FileMonitorActor 11 | import sangria.gateway.http.client.HttpClient 12 | import sangria.gateway.schema.materializer.{GatewayContext, GatewayMaterializer} 13 | import sangria.gateway.util.Logging 14 | 15 | import scala.concurrent.{ExecutionContext, Future} 16 | import scala.util.{Failure, Success} 17 | 18 | // TODO: on a timer reload all external schemas and check for changes 19 | class ReloadableSchemaProvider(config: AppConfig, client: HttpClient, mat: GatewayMaterializer)(implicit system: ActorSystem, ec: ExecutionContext, amat: Materializer) extends SchemaProvider[GatewayContext, Any] with Logging { 20 | val loader = new SchemaLoader(config, client, mat) 21 | val schemaRef = new AtomicReference[Option[SchemaInfo[GatewayContext, Any]]](None) 22 | 23 | system.actorOf(FileMonitorActor.props(config.watch.allFiles, config.watch.threshold, config.watch.allGlobs, reloadSchema)) 24 | 25 | private val producer = Source.actorRef[Boolean](100, OverflowStrategy.dropTail) 26 | private val runnableGraph = producer.toMat(BroadcastHub.sink(bufferSize = 256))(Keep.both) 27 | private val (changesPublisher, changesSource) = runnableGraph.run() 28 | 29 | val schemaChanges = Some(changesSource) 30 | 31 | def schemaInfo = 32 | schemaRef.get() match { 33 | case v @ Some(_) ⇒ Future.successful(v) 34 | case None ⇒ reloadSchema 35 | } 36 | 37 | def reloadSchema(files: Vector[File]): Unit = { 38 | logger.info(s"Schema files are changed: ${files mkString ", "}. Reloading schema") 39 | 40 | reloadSchema 41 | } 42 | 43 | def reloadSchema: Future[Option[SchemaInfo[GatewayContext, Any]]] = 44 | loader.loadSchema.andThen { 45 | case Success(Some(newSchema)) ⇒ 46 | schemaRef.get() match { 47 | case Some(currentSchema) ⇒ 48 | val changes = newSchema.schema.compare(currentSchema.schema) 49 | val renderedChanges = 50 | if (changes.nonEmpty) 51 | " with following changes:\n" + changes.map(c ⇒ " * " + c.description + (if (c.breakingChange) " (breaking)" else "")).mkString("\n") 52 | else 53 | " without any changes." 54 | 55 | changesPublisher ! true 56 | logger.info(s"Schema successfully reloaded$renderedChanges") 57 | case None ⇒ 58 | logger.info(s"Schema successfully loaded from files:\n${newSchema.files.map(f ⇒ " * " + f).mkString("\n")}") 59 | } 60 | 61 | schemaRef.set(Some(newSchema)) 62 | case Failure(error) ⇒ 63 | logger.error("Failed to load the schema", error) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/schema/SchemaLoader.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.schema 2 | 3 | import better.files.File 4 | import sangria.ast.Document 5 | import sangria.execution.Executor 6 | import sangria.execution.deferred.DeferredResolver 7 | import sangria.gateway.AppConfig 8 | import sangria.gateway.file.FileUtil 9 | import sangria.gateway.http.client.HttpClient 10 | import sangria.gateway.schema.materializer.{GatewayContext, GatewayMaterializer} 11 | import sangria.gateway.util.Logging 12 | import sangria.parser.QueryParser 13 | import sangria.schema.Schema 14 | import sangria.marshalling.circe._ 15 | 16 | import scala.concurrent.{ExecutionContext, Future} 17 | import scala.util.control.NonFatal 18 | import scala.util.{Failure, Success} 19 | 20 | class SchemaLoader(config: AppConfig, client: HttpClient, mat: GatewayMaterializer)(implicit ec: ExecutionContext) extends Logging { 21 | def loadSchema: Future[Option[SchemaInfo[GatewayContext, Any]]] = { 22 | val files = FileUtil.loadFiles(config.watch.allFiles, config.watch.allGlobs) 23 | 24 | if (files.nonEmpty) { 25 | val parsed = 26 | files.map { 27 | case (path, content) ⇒ path → QueryParser.parse(content) 28 | } 29 | 30 | val failed = parsed.collect {case (path, Failure(e)) ⇒ path → e} 31 | 32 | if (failed.nonEmpty) { 33 | failed.foreach { case (path, error) ⇒ 34 | logger.error(s"Can't parse file '$path':\n${error.getMessage}") 35 | } 36 | 37 | Future.successful(None) 38 | } else { 39 | val successful = parsed.collect {case (path, Success(doc)) ⇒ path → doc} 40 | val document = Document.merge(successful.map(_._2)) 41 | 42 | try { 43 | val info = 44 | for { 45 | ctx ← GatewayContext.loadContext(config, client, document) 46 | schema = Schema.buildFromAst(document, mat.schemaBuilder(ctx).validateSchemaWithException(document)) 47 | intro ← executeIntrospection(schema, ctx) 48 | } yield Some(SchemaInfo( 49 | schema, 50 | ctx, 51 | (), 52 | Nil, 53 | DeferredResolver.empty, 54 | schema.renderPretty, 55 | intro, 56 | files.map(_._1))) 57 | 58 | info.recover(handleError(files)) 59 | } catch { 60 | case e if handleError(files).isDefinedAt(e) ⇒ 61 | Future.successful(handleError(files)(e)) 62 | } 63 | } 64 | } else { 65 | logger.error("No schema files found!") 66 | Future.successful(None) 67 | } 68 | } 69 | 70 | private def handleError(files: Vector[(File, String)]): PartialFunction[Throwable, Option[SchemaInfo[GatewayContext, Any]]] = { 71 | case NonFatal(e) ⇒ 72 | logger.error(s"Can't create the schema from files: ${files.map(_._1).mkString(", ")}. " + e.getMessage) 73 | None 74 | } 75 | 76 | private def executeIntrospection(schema: Schema[GatewayContext, Any], ctx: GatewayContext) = 77 | Executor.execute(schema, sangria.introspection.introspectionQuery(schemaDescription = false, directiveRepeatableFlag = false), ctx) 78 | } 79 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/schema/SchemaProvider.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.schema 2 | 3 | import akka.NotUsed 4 | import akka.stream.scaladsl.Source 5 | import better.files.File 6 | import io.circe.Json 7 | import sangria.execution.{Executor, Middleware} 8 | import sangria.execution.deferred.DeferredResolver 9 | import sangria.schema.Schema 10 | 11 | import scala.concurrent.{Await, Future} 12 | 13 | trait SchemaProvider[Ctx, Val] { 14 | def schemaInfo: Future[Option[SchemaInfo[Ctx, Val]]] 15 | def schemaChanges: Option[Source[Boolean, NotUsed]] 16 | } 17 | 18 | case class SchemaInfo[Ctx, Val]( 19 | schema: Schema[Ctx, Val], 20 | ctx: Ctx, 21 | value: Val, 22 | middleware: List[Middleware[Ctx]], 23 | deferredResolver: DeferredResolver[Ctx] = DeferredResolver.empty, 24 | schemaRendered: String, 25 | schemaIntrospection: Json, 26 | files: Vector[File]) 27 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/schema/StaticSchemaProvider.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.schema 2 | 3 | import sangria.gateway.AppConfig 4 | import sangria.gateway.http.client.HttpClient 5 | import sangria.gateway.schema.materializer.{GatewayContext, GatewayMaterializer} 6 | 7 | import scala.concurrent.ExecutionContext 8 | 9 | class StaticSchemaProvider(config: AppConfig, client: HttpClient, mat: GatewayMaterializer)(implicit ec: ExecutionContext) extends SchemaProvider[GatewayContext, Any] { 10 | val loader = new SchemaLoader(config, client, mat) 11 | 12 | val schemaInfo = loader.loadSchema 13 | val schemaChanges = None 14 | } -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/schema/materializer/GatewayContext.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.schema.materializer 2 | 3 | import java.util.{Locale, Random} 4 | 5 | import com.github.javafaker.Faker 6 | 7 | import language.existentials 8 | import io.circe.Json 9 | import io.circe.optics.JsonPath.root 10 | import sangria.ast 11 | import sangria.gateway.AppConfig 12 | import sangria.gateway.http.client.HttpClient 13 | import sangria.gateway.json.CirceJsonPath 14 | import sangria.gateway.schema.materializer.directive.{BasicDirectiveProvider, FakerDirectiveProvider, GraphQLDirectiveProvider, OAuthClientCredentials} 15 | import sangria.gateway.util.Logging 16 | import sangria.schema._ 17 | import sangria.schema.ResolverBasedAstSchemaBuilder.resolveDirectives 18 | import sangria.marshalling.circe._ 19 | import sangria.marshalling.queryAst._ 20 | import sangria.marshalling.MarshallingUtil._ 21 | import sangria.gateway.schema.materializer.directive.HttpDirectiveProvider.{extractDelegatedHeaders, extractMap} 22 | 23 | import scala.concurrent.{ExecutionContext, Future} 24 | import scala.collection.JavaConverters._ 25 | 26 | case class GatewayContext(client: HttpClient, rnd: Option[Random], faker: Option[Faker], vars: Json, graphqlIncludes: Vector[GraphQLIncludedSchema], operationName: Option[String] = None, queryVars: Json = Json.obj(), originalHeaders: Seq[(String, String)] = Seq.empty) { 27 | import GatewayContext._ 28 | 29 | val allTypes = graphqlIncludes.flatMap(_.types) 30 | 31 | def request(c: Context[GatewayContext, _], schema: GraphQLIncludedSchema, query: ast.Document, variables: Json, extractName: String)(implicit ec: ExecutionContext): Future[Json] = { 32 | val fields = Vector("query" → Json.fromString(query.renderPretty)) 33 | val withVars = 34 | if (variables != Json.obj()) fields :+ ("variables" → variables) 35 | else fields 36 | 37 | val body = Json.fromFields(withVars) 38 | 39 | val url = GatewayContext.fillPlaceholders(Some(c), schema.include.url) 40 | val queryParams = schema.include.queryParams 41 | val oauthHead = oauthHeaders(schema.oauthToken) 42 | val headers = oauthHead ++ schema.include.headers ++ extractDelegatedHeaders(c, Some(schema.include.delegateHeaders)) 43 | 44 | client.request(HttpClient.Method.Post, url, queryParams, headers, Some("application/json" → body.noSpaces)).flatMap(GatewayContext.parseJson).map(resp ⇒ 45 | root.data.at(extractName).getOption(resp).flatten.get) 46 | } 47 | 48 | def findFields(name: String, typeName: String, includeFields: Option[Seq[String]], excludeFields: Option[Seq[String]]): List[MaterializedField[GatewayContext, _]] = 49 | graphqlIncludes.find(_.include.name == name).toList.flatMap { s ⇒ 50 | val tpe = s.schema.getOutputType(ast.NamedType(typeName), topLevel = true) 51 | val fields = tpe.toList 52 | .collect {case obj: ObjectLikeType[_, _] ⇒ obj} 53 | .flatMap { t ⇒ 54 | val withIncludes = includeFields match { 55 | case Some(inc) ⇒ t.uniqueFields.filter(f ⇒ includeFields.forall(_.contains(f.name))) 56 | case _ ⇒ t.uniqueFields 57 | } 58 | 59 | val withExcludes = excludeFields match { 60 | case Some(excl) ⇒ withIncludes.filterNot(f ⇒ excl.contains(f.name)) 61 | case _ ⇒ withIncludes 62 | } 63 | 64 | withExcludes.asInstanceOf[Vector[Field[GatewayContext, Any]]] 65 | } 66 | 67 | fields.map(f ⇒ MaterializedField(s, f.copy(astDirectives = Vector(ast.Directive("delegate", Vector.empty))))) 68 | } 69 | } 70 | 71 | object GatewayContext extends Logging { 72 | val envJson = Json.obj(System.getenv().asScala.mapValues(Json.fromString).toSeq: _*) 73 | 74 | def fillPlaceholders(ctx: Option[Context[GatewayContext, _]], value: String, cachedArgs: Option[Json] = None, elem: Json = Json.Null): String = { 75 | lazy val args = cachedArgs orElse ctx.map(c ⇒ convertArgs(c.args, c.astFields.head)) getOrElse Json.obj() 76 | 77 | PlaceholderRegExp.findAllMatchIn(value).toVector.foldLeft(value) { case (acc, p) ⇒ 78 | val placeholder = p.group(0) 79 | 80 | val idx = p.group(1).indexOf(".") 81 | 82 | if (idx < 0) throw new IllegalStateException(s"Invalid placeholder '$placeholder'. It should contain two parts: scope (like value or ctx) and extractor (name of the field or JSON path) separated byt dot (.).") 83 | 84 | val (scope, selectorWithDot) = p.group(1).splitAt(idx) 85 | val selector = selectorWithDot.substring(1) 86 | 87 | def unsupported(s: String) = throw new IllegalStateException(s"Unsupported placeholder scope '$s'. Supported scopes are: value, ctx, arg, elem.") 88 | 89 | val source = scope match { 90 | case "value" ⇒ ctx.getOrElse(unsupported("value")).value.asInstanceOf[Json] 91 | case "ctx" ⇒ ctx.getOrElse(unsupported("ctx")).ctx.vars 92 | case "arg" ⇒ 93 | ctx.getOrElse(unsupported("arg")) 94 | args 95 | case "elem" ⇒ elem 96 | case "env" ⇒ envJson 97 | case s ⇒ unsupported(s) 98 | } 99 | 100 | val value = 101 | if (selector.startsWith("$")) 102 | render(CirceJsonPath.query(source, selector)) 103 | else 104 | source.get(selector).map(render).getOrElse("") 105 | 106 | acc.replace(placeholder, value) 107 | } 108 | } 109 | 110 | def parseJson(resp: HttpClient.HttpResponse)(implicit ec: ExecutionContext) = 111 | if (resp.isSuccessful) 112 | resp.asString.map(s ⇒ { 113 | io.circe.parser.parse(s).fold({ e ⇒ 114 | logger.error(s"Failed to parse JSON response for successful request (${resp.statusCode} ${resp.debugInfo}). Body: $s", e) 115 | 116 | throw e 117 | }, identity) 118 | }) 119 | else 120 | Future.failed(new IllegalStateException(s"Failed HTTP request with status code ${resp.statusCode}. ${resp.debugInfo}")) 121 | 122 | val rootValueLoc = Set(DirectiveLocation.Schema) 123 | 124 | def rootValue(schemaAst: ast.Document) = { 125 | val values = resolveDirectives(schemaAst, 126 | GenericDirectiveResolver(BasicDirectiveProvider.Dirs.JsonConst, rootValueLoc, 127 | c ⇒ Some(io.circe.parser.parse(c arg BasicDirectiveProvider.Args.JsonValue).fold(throw _, identity))), 128 | GenericDynamicDirectiveResolver[Json, Json]("const", rootValueLoc, 129 | c ⇒ c.args.get("value"))) 130 | 131 | Json.fromFields(values.collect { 132 | case json: Json if json.isObject ⇒ json.asObject.get.toIterable 133 | }.flatten) 134 | } 135 | 136 | def graphqlIncludes(schemaAst: ast.Document) = { 137 | import GraphQLDirectiveProvider.{Args, Dirs} 138 | 139 | resolveDirectives(schemaAst, 140 | GenericDirectiveResolver(Dirs.IncludeSchema, resolve = c ⇒ 141 | c.withArgs(Args.Name, Args.Url, Args.Headers, Args.DelegateHeaders, Args.QueryParams, Args.OAuth)((name, url, headers, delegateHeaders, queryParams, oauth) ⇒ 142 | Some(GraphQLInclude(url, name, extractMap(None, headers), extractMap(None, queryParams), delegateHeaders getOrElse Seq.empty, oauth))))) 143 | } 144 | 145 | def fakerConfig(schemaAst: ast.Document) = { 146 | import sangria.gateway.schema.materializer.directive.FakerDirectiveProvider._ 147 | 148 | resolveDirectives(schemaAst, 149 | GenericDirectiveResolver(Dirs.FakeConfig, 150 | resolve = c ⇒ Some(c.arg(Args.Locale).map(Locale.forLanguageTag) → c.arg(Args.Seed)))).headOption.getOrElse(None → None) 151 | } 152 | 153 | def loadIncludedSchemas(client: HttpClient, includes: Vector[GraphQLInclude])(implicit ec: ExecutionContext): Future[Vector[GraphQLIncludedSchema]] = { 154 | val loaded = 155 | includes.map { include ⇒ 156 | include.oauth match { 157 | case Some(oauth) ⇒ 158 | for { 159 | token ← loadOAuthToken(client, oauth) 160 | resp ← loadSchemaIntorospection(client, include, token) 161 | } yield GraphQLIncludedSchema(include, Schema.buildFromIntrospection(resp), token) 162 | 163 | case None ⇒ 164 | loadSchemaIntorospection(client, include, None) 165 | .map(resp ⇒ GraphQLIncludedSchema(include, Schema.buildFromIntrospection(resp), None)) 166 | } 167 | } 168 | 169 | Future.sequence(loaded) 170 | } 171 | 172 | def loadSchemaIntorospection(client: HttpClient, include: GraphQLInclude, oauthToken: Option[String])(implicit ec: ExecutionContext) = { 173 | val introspectionBody = Json.obj("query" → Json.fromString(sangria.introspection.introspectionQuery(schemaDescription = false, directiveRepeatableFlag = false).renderPretty)) 174 | val url = GatewayContext.fillPlaceholders(None, include.url) 175 | val oauthHead = oauthHeaders(oauthToken) 176 | 177 | client.request(HttpClient.Method.Post, url, include.queryParams, include.headers ++ oauthHead, Some("application/json" → introspectionBody.noSpaces)).flatMap(parseJson) 178 | } 179 | 180 | def oauthHeaders(oauthToken: Option[String]) = oauthToken match { 181 | case Some(token) ⇒ Seq("Authorization" → s"Bearer $token") 182 | case _ ⇒ Seq.empty 183 | } 184 | 185 | def loadOAuthToken(client: HttpClient, credentials: OAuthClientCredentials)(implicit ec: ExecutionContext) = { 186 | val url = GatewayContext.fillPlaceholders(None, credentials.url) 187 | val clientId = GatewayContext.fillPlaceholders(None, credentials.clientId) 188 | val clientSecret = GatewayContext.fillPlaceholders(None, credentials.clientSecret) 189 | val scopes = credentials.scopes.map(GatewayContext.fillPlaceholders(None, _)) 190 | 191 | client.oauthClientCredentials(url, clientId, clientSecret, scopes).flatMap(parseJson) 192 | .map(json ⇒ json.asObject.get.apply("access_token").flatMap(_.asString)) 193 | } 194 | 195 | def loadContext(config: AppConfig, client: HttpClient, schemaAst: ast.Document)(implicit ec: ExecutionContext): Future[GatewayContext] = { 196 | val includes = graphqlIncludes(schemaAst) 197 | val vars = rootValue(schemaAst) 198 | val (fakerLocale, fakerSeed) = fakerConfig(schemaAst) 199 | 200 | val (rnd, faker) = 201 | if (config isEnabled "faker") { 202 | val rnd = fakerSeed.fold(new Random(System.currentTimeMillis()))(s ⇒ new Random(s.toLong)) 203 | 204 | Some(rnd) → Some(fakerLocale.fold(new Faker(rnd))(l ⇒ new Faker(l, rnd))) 205 | } else None → None 206 | 207 | loadIncludedSchemas(client, includes).map(GatewayContext(client, rnd, faker, vars, _)) 208 | } 209 | 210 | def namedType(tpe: OutputType[_]): OutputType[_] = tpe match { 211 | case ListType(of) ⇒ namedType(of) 212 | case OptionType(of) ⇒ namedType(of) 213 | case t ⇒ t 214 | } 215 | 216 | def convertArgs(args: Args, field: ast.Field): Json = 217 | Json.fromFields(args.raw.keys.flatMap(name ⇒ field.arguments.find(_.name == name).map(a ⇒ a.name → a.value.convertMarshaled[Json]))) 218 | 219 | private val PlaceholderRegExp = """\$\{([^}]+)\}""".r 220 | 221 | // TODO: improve :) 222 | def render(value: Json) = value.fold( 223 | jsonNull = "null", 224 | jsonBoolean = "" + _, 225 | jsonNumber = "" + _.toBigDecimal.get, 226 | jsonString = identity, 227 | jsonArray = "" + _, 228 | jsonObject = "" + _) 229 | 230 | implicit class JsonOps(value: Json) { 231 | def get(key: String) = value match { 232 | case json: Json if json.isObject ⇒ json.asObject.get(key) 233 | case _ ⇒ None 234 | } 235 | } 236 | } 237 | 238 | case class GraphQLInclude(url: String, name: String, headers: Seq[(String, String)], queryParams: Seq[(String, String)], delegateHeaders: Seq[String], oauth: Option[OAuthClientCredentials]) 239 | case class GraphQLIncludedSchema(include: GraphQLInclude, schema: Schema[_, _], oauthToken: Option[String]) extends MatOrigin { 240 | private val rootTypeNames = Set(schema.query.name) ++ schema.mutation.map(_.name).toSet ++ schema.subscription.map(_.name).toSet 241 | 242 | val types = schema.allTypes.values 243 | .filterNot(t ⇒ Schema.isBuiltInType(t.name) || rootTypeNames.contains(t.name)).toVector 244 | .map { 245 | case t: ObjectType[_, _] ⇒ 246 | t.withInstanceCheck((value, _, tpe) ⇒ value match { 247 | case json: Json ⇒ root.__typename.string.getOption(json).contains(tpe.name) 248 | case _ ⇒ false 249 | }) 250 | 251 | case t ⇒ t 252 | } 253 | .map(MaterializedType(this, _)) 254 | 255 | def description = s"included schema '${include.name}'" 256 | } 257 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/schema/materializer/GatewayMaterializer.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.schema.materializer 2 | 3 | import sangria.schema._ 4 | import sangria.gateway.schema.materializer.directive.DirectiveProvider 5 | 6 | import scala.concurrent.ExecutionContext 7 | 8 | class GatewayMaterializer(directiveProviders: Seq[DirectiveProvider])(implicit ec: ExecutionContext) { 9 | def commonResolvers(ctx: GatewayContext, ar: Seq[PartialFunction[Context[GatewayContext, _], Action[GatewayContext, Any]]]) = Seq[AstSchemaResolver[GatewayContext]]( 10 | AnyFieldResolver { 11 | case origin if !origin.isInstanceOf[ExistingSchemaOrigin[_, _]] ⇒ c ⇒ { 12 | val fn = ar.find(_.isDefinedAt(c)).getOrElse(throw new SchemaMaterializationException(s"Field resolver is not defined. Unable to handle value `${c.value}`.")) 13 | 14 | fn(c) 15 | } 16 | }) 17 | 18 | def schemaBuilder(ctx: GatewayContext) = { 19 | val anyResolvers = directiveProviders.flatMap(_.anyResolver) 20 | val resolvers = directiveProviders.flatMap(_.resolvers(ctx)) ++ commonResolvers(ctx, anyResolvers) 21 | 22 | AstSchemaBuilder.resolverBased[GatewayContext](resolvers: _*) 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/schema/materializer/directive/BasicDirectiveProvider.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.schema.materializer.directive 2 | 3 | import io.circe.Json 4 | import sangria.gateway.json.CirceJsonPath 5 | import sangria.gateway.schema.CustomScalars 6 | import sangria.gateway.schema.materializer.GatewayContext 7 | import sangria.gateway.schema.materializer.GatewayContext._ 8 | import sangria.schema.ResolverBasedAstSchemaBuilder.{extractFieldValue, extractValue} 9 | import sangria.schema._ 10 | import sangria.marshalling.circe._ 11 | 12 | import scala.concurrent.ExecutionContext 13 | 14 | class BasicDirectiveProvider(implicit ec: ExecutionContext) extends DirectiveProvider { 15 | import BasicDirectiveProvider._ 16 | 17 | def resolvers(ctx: GatewayContext) = Seq( 18 | AdditionalTypes(CustomScalars.DateTimeType), 19 | 20 | DirectiveResolver(Dirs.Context, c ⇒ c.withArgs(Args.NameOpt, Args.Path) { (name, path) ⇒ 21 | name 22 | .map(n ⇒ extractValue(c.ctx.field.fieldType, c.ctx.ctx.vars.get(n))) 23 | .orElse(path.map(p ⇒ extractValue(c.ctx.field.fieldType, Some(CirceJsonPath.query(c.ctx.ctx.vars, p))))) 24 | .getOrElse(throw SchemaMaterializationException(s"Can't find a directive argument 'path' or 'name'.")) 25 | }), 26 | 27 | DirectiveResolver(Dirs.Value, c ⇒ c.withArgs(Args.NameOpt, Args.Path) { (name, path) ⇒ 28 | def extract(value: Any) = 29 | name 30 | .map(n ⇒ extractValue(c.ctx.field.fieldType, value.asInstanceOf[Json].get(n))) 31 | .orElse(path.map(p ⇒ extractValue(c.ctx.field.fieldType, Some(CirceJsonPath.query(value.asInstanceOf[Json], p))))) 32 | .getOrElse(throw SchemaMaterializationException(s"Can't find a directive argument 'path' or 'name'.")) 33 | 34 | c.lastValue map (_.map(extract)) getOrElse extract(c.ctx.value) 35 | }), 36 | 37 | DirectiveResolver(Dirs.Arg, c ⇒ 38 | extractValue(c.ctx.field.fieldType, 39 | convertArgs(c.ctx.args, c.ctx.astFields.head).get(c arg Args.NameReq))), 40 | 41 | DynamicDirectiveResolver[GatewayContext, Json]("const", c ⇒ 42 | extractValue(c.ctx.field.fieldType, Some(c.args.get("value") match { 43 | case Some(json: Json) if json.isString ⇒ Json.fromString(GatewayContext.fillPlaceholders(Some(c.ctx), json.asString.get)) 44 | case Some(jv) ⇒ jv 45 | case _ ⇒ Json.Null 46 | }))), 47 | 48 | DirectiveResolver(Dirs.JsonConst, c ⇒ 49 | extractValue(c.ctx.field.fieldType, 50 | Some(io.circe.parser.parse(GatewayContext.fillPlaceholders(Some(c.ctx), c arg Args.JsonValue)).fold(throw _, identity))))) 51 | 52 | override val anyResolver = Some({ 53 | case c if c.value.isInstanceOf[Json] ⇒ ResolverBasedAstSchemaBuilder.extractFieldValue[GatewayContext, Json](c) 54 | }) 55 | } 56 | 57 | object BasicDirectiveProvider { 58 | object Args { 59 | val NameOpt = Argument("name", OptionInputType(StringType)) 60 | val NameReq = Argument("name", StringType) 61 | val Path = Argument("path", OptionInputType(StringType)) 62 | val JsonValue = Argument("value", StringType) 63 | } 64 | 65 | object Dirs { 66 | val Context = Directive("context", 67 | arguments = Args.NameOpt :: Args.Path :: Nil, 68 | locations = Set(DirectiveLocation.FieldDefinition)) 69 | 70 | val Value = Directive("value", 71 | arguments = Args.NameOpt :: Args.Path :: Nil, 72 | locations = Set(DirectiveLocation.FieldDefinition)) 73 | 74 | val Arg = Directive("arg", 75 | arguments = Args.NameReq :: Nil, 76 | locations = Set(DirectiveLocation.FieldDefinition)) 77 | 78 | val JsonConst = Directive("jsonValue", 79 | arguments = Args.JsonValue :: Nil, 80 | locations = Set(DirectiveLocation.FieldDefinition, DirectiveLocation.Schema)) 81 | } 82 | } 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/schema/materializer/directive/DirectiveProvider.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.schema.materializer.directive 2 | 3 | import sangria.gateway.schema.materializer.GatewayContext 4 | import sangria.schema.{Action, AstSchemaResolver, Context} 5 | 6 | trait DirectiveProvider { 7 | def resolvers(ctx: GatewayContext): Seq[AstSchemaResolver[GatewayContext]] 8 | def anyResolver: Option[PartialFunction[Context[GatewayContext, _], Action[GatewayContext, Any]]] = None 9 | } 10 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/schema/materializer/directive/FakerDirectiveProvider.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.schema.materializer.directive 2 | 3 | import java.time.{ZoneId, ZonedDateTime} 4 | import java.util.concurrent.TimeUnit 5 | import java.util.Random 6 | import java.util.regex.Pattern 7 | 8 | import com.github.javafaker.Faker 9 | import sangria.gateway.schema.CustomScalars 10 | import sangria.gateway.schema.materializer.GatewayContext 11 | import sangria.renderer.SchemaRenderer 12 | import sangria.schema._ 13 | 14 | class FakerDirectiveProvider extends DirectiveProvider { 15 | import FakerDirectiveProvider._ 16 | 17 | def resolvers(ctx: GatewayContext) = Seq( 18 | AdditionalDirectives(Seq(Dirs.FakeConfig)), 19 | DirectiveResolver(Dirs.Fake, c ⇒ c.withArgs(Args.Expr, Args.Type, Args.MinElems, Args.MaxElems, Args.Past, Args.Future) { (expr, tpe, min, max, past, future) ⇒ 20 | FakeValue(expr orElse tpe, min, max, past, future, c.ctx.ctx.rnd.get, c.ctx.ctx.faker.get).coerce(c.ctx) 21 | }) 22 | ) 23 | 24 | override def anyResolver = Some({ 25 | case c if c.value.isInstanceOf[FakeValue] ⇒ c.value.asInstanceOf[FakeValue].coerce(c) 26 | }) 27 | } 28 | 29 | object FakerDirectiveProvider { 30 | val FakeType = 31 | EnumType[String]("FakeType", Some("The type of the content generated by the faker"), List( 32 | EnumValue("AddressBuildingNumber", Some("Represents faker expression `address.buildingNumber`."), "address.buildingNumber"), 33 | EnumValue("AddressCity", Some("Represents faker expression `address.city`."), "address.city"), 34 | EnumValue("AddressCityName", Some("Represents faker expression `address.cityName`."), "address.cityName"), 35 | EnumValue("AddressCityPrefix", Some("Represents faker expression `address.cityPrefix`."), "address.cityPrefix"), 36 | EnumValue("AddressCitySuffix", Some("Represents faker expression `address.citySuffix`."), "address.citySuffix"), 37 | EnumValue("AddressCountry", Some("Represents faker expression `address.country`."), "address.country"), 38 | EnumValue("AddressCountryCode", Some("Represents faker expression `address.countryCode`."), "address.countryCode"), 39 | EnumValue("AddressFirstName", Some("Represents faker expression `address.firstName`."), "address.firstName"), 40 | EnumValue("AddressFullAddress", Some("Represents faker expression `address.fullAddress`."), "address.fullAddress"), 41 | EnumValue("AddressLastName", Some("Represents faker expression `address.lastName`."), "address.lastName"), 42 | EnumValue("AddressLatitude", Some("Represents faker expression `address.latitude`."), "address.latitude"), 43 | EnumValue("AddressLongitude", Some("Represents faker expression `address.longitude`."), "address.longitude"), 44 | EnumValue("AddressSecondaryAddress", Some("Represents faker expression `address.secondaryAddress`."), "address.secondaryAddress"), 45 | EnumValue("AddressState", Some("Represents faker expression `address.state`."), "address.state"), 46 | EnumValue("AddressStateAbbr", Some("Represents faker expression `address.stateAbbr`."), "address.stateAbbr"), 47 | EnumValue("AddressStreetAddress", Some("Represents faker expression `address.streetAddress`."), "address.streetAddress"), 48 | EnumValue("AddressStreetAddressNumber", Some("Represents faker expression `address.streetAddressNumber`."), "address.streetAddressNumber"), 49 | EnumValue("AddressStreetName", Some("Represents faker expression `address.streetName`."), "address.streetName"), 50 | EnumValue("AddressStreetPrefix", Some("Represents faker expression `address.streetPrefix`."), "address.streetPrefix"), 51 | EnumValue("AddressStreetSuffix", Some("Represents faker expression `address.streetSuffix`."), "address.streetSuffix"), 52 | EnumValue("AddressTimeZone", Some("Represents faker expression `address.timeZone`."), "address.timeZone"), 53 | EnumValue("AddressZipCode", Some("Represents faker expression `address.zipCode`."), "address.zipCode"), 54 | EnumValue("AncientGod", Some("Represents faker expression `ancient.god`."), "ancient.god"), 55 | EnumValue("AncientHero", Some("Represents faker expression `ancient.hero`."), "ancient.hero"), 56 | EnumValue("AncientPrimordial", Some("Represents faker expression `ancient.primordial`."), "ancient.primordial"), 57 | EnumValue("AncientTitan", Some("Represents faker expression `ancient.titan`."), "ancient.titan"), 58 | EnumValue("AppAuthor", Some("Represents faker expression `app.author`."), "app.author"), 59 | EnumValue("AppName", Some("Represents faker expression `app.name`."), "app.name"), 60 | EnumValue("AppVersion", Some("Represents faker expression `app.version`."), "app.version"), 61 | EnumValue("ArtistName", Some("Represents faker expression `artist.name`."), "artist.name"), 62 | EnumValue("AvatarImage", Some("Represents faker expression `avatar.image`."), "avatar.image"), 63 | EnumValue("BeerHop", Some("Represents faker expression `beer.hop`."), "beer.hop"), 64 | EnumValue("BeerMalt", Some("Represents faker expression `beer.malt`."), "beer.malt"), 65 | EnumValue("BeerName", Some("Represents faker expression `beer.name`."), "beer.name"), 66 | EnumValue("BeerStyle", Some("Represents faker expression `beer.style`."), "beer.style"), 67 | EnumValue("BeerYeast", Some("Represents faker expression `beer.yeast`."), "beer.yeast"), 68 | EnumValue("BookAuthor", Some("Represents faker expression `book.author`."), "book.author"), 69 | EnumValue("BookGenre", Some("Represents faker expression `book.genre`."), "book.genre"), 70 | EnumValue("BookPublisher", Some("Represents faker expression `book.publisher`."), "book.publisher"), 71 | EnumValue("BookTitle", Some("Represents faker expression `book.title`."), "book.title"), 72 | EnumValue("BoolBool", Some("Represents faker expression `bool.bool`."), "bool.bool"), 73 | EnumValue("BusinessCreditCardExpiry", Some("Represents faker expression `business.creditCardExpiry`."), "business.creditCardExpiry"), 74 | EnumValue("BusinessCreditCardNumber", Some("Represents faker expression `business.creditCardNumber`."), "business.creditCardNumber"), 75 | EnumValue("BusinessCreditCardType", Some("Represents faker expression `business.creditCardType`."), "business.creditCardType"), 76 | EnumValue("CatBreed", Some("Represents faker expression `cat.breed`."), "cat.breed"), 77 | EnumValue("CatName", Some("Represents faker expression `cat.name`."), "cat.name"), 78 | EnumValue("CatRegistry", Some("Represents faker expression `cat.registry`."), "cat.registry"), 79 | EnumValue("ChuckNorrisFact", Some("Represents faker expression `chuckNorris.fact`."), "chuckNorris.fact"), 80 | EnumValue("CodeAsin", Some("Represents faker expression `code.asin`."), "code.asin"), 81 | EnumValue("CodeEan13", Some("Represents faker expression `code.ean13`."), "code.ean13"), 82 | EnumValue("CodeEan8", Some("Represents faker expression `code.ean8`."), "code.ean8"), 83 | EnumValue("CodeGtin13", Some("Represents faker expression `code.gtin13`."), "code.gtin13"), 84 | EnumValue("CodeGtin8", Some("Represents faker expression `code.gtin8`."), "code.gtin8"), 85 | EnumValue("CodeImei", Some("Represents faker expression `code.imei`."), "code.imei"), 86 | EnumValue("CodeIsbn10", Some("Represents faker expression `code.isbn10`."), "code.isbn10"), 87 | EnumValue("CodeIsbn13", Some("Represents faker expression `code.isbn13`."), "code.isbn13"), 88 | EnumValue("CodeIsbnGroup", Some("Represents faker expression `code.isbnGroup`."), "code.isbnGroup"), 89 | EnumValue("CodeIsbnGs1", Some("Represents faker expression `code.isbnGs1`."), "code.isbnGs1"), 90 | EnumValue("CodeIsbnRegistrant", Some("Represents faker expression `code.isbnRegistrant`."), "code.isbnRegistrant"), 91 | EnumValue("ColorName", Some("Represents faker expression `color.name`."), "color.name"), 92 | EnumValue("CommerceColor", Some("Represents faker expression `commerce.color`."), "commerce.color"), 93 | EnumValue("CommerceDepartment", Some("Represents faker expression `commerce.department`."), "commerce.department"), 94 | EnumValue("CommerceMaterial", Some("Represents faker expression `commerce.material`."), "commerce.material"), 95 | EnumValue("CommercePrice", Some("Represents faker expression `commerce.price`."), "commerce.price"), 96 | EnumValue("CommerceProductName", Some("Represents faker expression `commerce.productName`."), "commerce.productName"), 97 | EnumValue("CommercePromotionCode", Some("Represents faker expression `commerce.promotionCode`."), "commerce.promotionCode"), 98 | EnumValue("CompanyBs", Some("Represents faker expression `company.bs`."), "company.bs"), 99 | EnumValue("CompanyBuzzword", Some("Represents faker expression `company.buzzword`."), "company.buzzword"), 100 | EnumValue("CompanyCatchPhrase", Some("Represents faker expression `company.catchPhrase`."), "company.catchPhrase"), 101 | EnumValue("CompanyIndustry", Some("Represents faker expression `company.industry`."), "company.industry"), 102 | EnumValue("CompanyLogo", Some("Represents faker expression `company.logo`."), "company.logo"), 103 | EnumValue("CompanyName", Some("Represents faker expression `company.name`."), "company.name"), 104 | EnumValue("CompanyProfession", Some("Represents faker expression `company.profession`."), "company.profession"), 105 | EnumValue("CompanySuffix", Some("Represents faker expression `company.suffix`."), "company.suffix"), 106 | EnumValue("CompanyUrl", Some("Represents faker expression `company.url`."), "company.url"), 107 | EnumValue("CryptoMd5", Some("Represents faker expression `crypto.md5`."), "crypto.md5"), 108 | EnumValue("CryptoSha1", Some("Represents faker expression `crypto.sha1`."), "crypto.sha1"), 109 | EnumValue("CryptoSha256", Some("Represents faker expression `crypto.sha256`."), "crypto.sha256"), 110 | EnumValue("CryptoSha512", Some("Represents faker expression `crypto.sha512`."), "crypto.sha512"), 111 | EnumValue("CurrencyCode", Some("Represents faker expression `currency.code`."), "currency.code"), 112 | EnumValue("CurrencyName", Some("Represents faker expression `currency.name`."), "currency.name"), 113 | EnumValue("DemographicDemonym", Some("Represents faker expression `demographic.demonym`."), "demographic.demonym"), 114 | EnumValue("DemographicEducationalAttainment", Some("Represents faker expression `demographic.educationalAttainment`."), "demographic.educationalAttainment"), 115 | EnumValue("DemographicMaritalStatus", Some("Represents faker expression `demographic.maritalStatus`."), "demographic.maritalStatus"), 116 | EnumValue("DemographicRace", Some("Represents faker expression `demographic.race`."), "demographic.race"), 117 | EnumValue("DemographicSex", Some("Represents faker expression `demographic.sex`."), "demographic.sex"), 118 | EnumValue("DogAge", Some("Represents faker expression `dog.age`."), "dog.age"), 119 | EnumValue("DogBreed", Some("Represents faker expression `dog.breed`."), "dog.breed"), 120 | EnumValue("DogCoatLength", Some("Represents faker expression `dog.coatLength`."), "dog.coatLength"), 121 | EnumValue("DogGender", Some("Represents faker expression `dog.gender`."), "dog.gender"), 122 | EnumValue("DogMemePhrase", Some("Represents faker expression `dog.memePhrase`."), "dog.memePhrase"), 123 | EnumValue("DogName", Some("Represents faker expression `dog.name`."), "dog.name"), 124 | EnumValue("DogSize", Some("Represents faker expression `dog.size`."), "dog.size"), 125 | EnumValue("DogSound", Some("Represents faker expression `dog.sound`."), "dog.sound"), 126 | EnumValue("DragonBallCharacter", Some("Represents faker expression `dragonBall.character`."), "dragonBall.character"), 127 | EnumValue("EducatorCampus", Some("Represents faker expression `educator.campus`."), "educator.campus"), 128 | EnumValue("EducatorCourse", Some("Represents faker expression `educator.course`."), "educator.course"), 129 | EnumValue("EducatorSecondarySchool", Some("Represents faker expression `educator.secondarySchool`."), "educator.secondarySchool"), 130 | EnumValue("EducatorUniversity", Some("Represents faker expression `educator.university`."), "educator.university"), 131 | EnumValue("EsportsEvent", Some("Represents faker expression `esports.event`."), "esports.event"), 132 | EnumValue("EsportsGame", Some("Represents faker expression `esports.game`."), "esports.game"), 133 | EnumValue("EsportsLeague", Some("Represents faker expression `esports.league`."), "esports.league"), 134 | EnumValue("EsportsPlayer", Some("Represents faker expression `esports.player`."), "esports.player"), 135 | EnumValue("EsportsTeam", Some("Represents faker expression `esports.team`."), "esports.team"), 136 | EnumValue("FileExtension", Some("Represents faker expression `file.extension`."), "file.extension"), 137 | EnumValue("FileFileName", Some("Represents faker expression `file.fileName`."), "file.fileName"), 138 | EnumValue("FileMimeType", Some("Represents faker expression `file.mimeType`."), "file.mimeType"), 139 | EnumValue("FinanceBic", Some("Represents faker expression `finance.bic`."), "finance.bic"), 140 | EnumValue("FinanceCreditCard", Some("Represents faker expression `finance.creditCard`."), "finance.creditCard"), 141 | EnumValue("FinanceIban", Some("Represents faker expression `finance.iban`."), "finance.iban"), 142 | EnumValue("FoodIngredient", Some("Represents faker expression `food.ingredient`."), "food.ingredient"), 143 | EnumValue("FoodMeasurement", Some("Represents faker expression `food.measurement`."), "food.measurement"), 144 | EnumValue("FoodSpice", Some("Represents faker expression `food.spice`."), "food.spice"), 145 | EnumValue("FriendsCharacter", Some("Represents faker expression `friends.character`."), "friends.character"), 146 | EnumValue("FriendsLocation", Some("Represents faker expression `friends.location`."), "friends.location"), 147 | EnumValue("FriendsQuote", Some("Represents faker expression `friends.quote`."), "friends.quote"), 148 | EnumValue("FunnyNameName", Some("Represents faker expression `funnyName.name`."), "funnyName.name"), 149 | EnumValue("GameOfThronesCharacter", Some("Represents faker expression `gameOfThrones.character`."), "gameOfThrones.character"), 150 | EnumValue("GameOfThronesCity", Some("Represents faker expression `gameOfThrones.city`."), "gameOfThrones.city"), 151 | EnumValue("GameOfThronesDragon", Some("Represents faker expression `gameOfThrones.dragon`."), "gameOfThrones.dragon"), 152 | EnumValue("GameOfThronesHouse", Some("Represents faker expression `gameOfThrones.house`."), "gameOfThrones.house"), 153 | EnumValue("GameOfThronesQuote", Some("Represents faker expression `gameOfThrones.quote`."), "gameOfThrones.quote"), 154 | EnumValue("HackerAbbreviation", Some("Represents faker expression `hacker.abbreviation`."), "hacker.abbreviation"), 155 | EnumValue("HackerAdjective", Some("Represents faker expression `hacker.adjective`."), "hacker.adjective"), 156 | EnumValue("HackerIngverb", Some("Represents faker expression `hacker.ingverb`."), "hacker.ingverb"), 157 | EnumValue("HackerNoun", Some("Represents faker expression `hacker.noun`."), "hacker.noun"), 158 | EnumValue("HackerVerb", Some("Represents faker expression `hacker.verb`."), "hacker.verb"), 159 | EnumValue("HarryPotterBook", Some("Represents faker expression `harryPotter.book`."), "harryPotter.book"), 160 | EnumValue("HarryPotterCharacter", Some("Represents faker expression `harryPotter.character`."), "harryPotter.character"), 161 | EnumValue("HarryPotterLocation", Some("Represents faker expression `harryPotter.location`."), "harryPotter.location"), 162 | EnumValue("HarryPotterQuote", Some("Represents faker expression `harryPotter.quote`."), "harryPotter.quote"), 163 | EnumValue("HipsterWord", Some("Represents faker expression `hipster.word`."), "hipster.word"), 164 | EnumValue("HitchhikersGuideToTheGalaxyCharacter", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.character`."), "hitchhikersGuideToTheGalaxy.character"), 165 | EnumValue("HitchhikersGuideToTheGalaxyLocation", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.location`."), "hitchhikersGuideToTheGalaxy.location"), 166 | EnumValue("HitchhikersGuideToTheGalaxyMarvinQuote", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.marvinQuote`."), "hitchhikersGuideToTheGalaxy.marvinQuote"), 167 | EnumValue("HitchhikersGuideToTheGalaxyPlanet", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.planet`."), "hitchhikersGuideToTheGalaxy.planet"), 168 | EnumValue("HitchhikersGuideToTheGalaxyQuote", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.quote`."), "hitchhikersGuideToTheGalaxy.quote"), 169 | EnumValue("HitchhikersGuideToTheGalaxySpecie", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.specie`."), "hitchhikersGuideToTheGalaxy.specie"), 170 | EnumValue("HitchhikersGuideToTheGalaxyStarship", Some("Represents faker expression `hitchhikersGuideToTheGalaxy.starship`."), "hitchhikersGuideToTheGalaxy.starship"), 171 | EnumValue("HobbitCharacter", Some("Represents faker expression `hobbit.character`."), "hobbit.character"), 172 | EnumValue("HobbitLocation", Some("Represents faker expression `hobbit.location`."), "hobbit.location"), 173 | EnumValue("HobbitQuote", Some("Represents faker expression `hobbit.quote`."), "hobbit.quote"), 174 | EnumValue("HobbitThorinsCompany", Some("Represents faker expression `hobbit.thorinsCompany`."), "hobbit.thorinsCompany"), 175 | EnumValue("HowIMetYourMotherCatchPhrase", Some("Represents faker expression `howIMetYourMother.catchPhrase`."), "howIMetYourMother.catchPhrase"), 176 | EnumValue("HowIMetYourMotherCharacter", Some("Represents faker expression `howIMetYourMother.character`."), "howIMetYourMother.character"), 177 | EnumValue("HowIMetYourMotherHighFive", Some("Represents faker expression `howIMetYourMother.highFive`."), "howIMetYourMother.highFive"), 178 | EnumValue("HowIMetYourMotherQuote", Some("Represents faker expression `howIMetYourMother.quote`."), "howIMetYourMother.quote"), 179 | EnumValue("IdNumberInvalid", Some("Represents faker expression `idNumber.invalid`."), "idNumber.invalid"), 180 | EnumValue("IdNumberInvalidSvSeSsn", Some("Represents faker expression `idNumber.invalidSvSeSsn`."), "idNumber.invalidSvSeSsn"), 181 | EnumValue("IdNumberSsnValid", Some("Represents faker expression `idNumber.ssnValid`."), "idNumber.ssnValid"), 182 | EnumValue("IdNumberValid", Some("Represents faker expression `idNumber.valid`."), "idNumber.valid"), 183 | EnumValue("IdNumberValidSvSeSsn", Some("Represents faker expression `idNumber.validSvSeSsn`."), "idNumber.validSvSeSsn"), 184 | EnumValue("InternetAvatar", Some("Represents faker expression `internet.avatar`."), "internet.avatar"), 185 | EnumValue("InternetDomainName", Some("Represents faker expression `internet.domainName`."), "internet.domainName"), 186 | EnumValue("InternetDomainSuffix", Some("Represents faker expression `internet.domainSuffix`."), "internet.domainSuffix"), 187 | EnumValue("InternetDomainWord", Some("Represents faker expression `internet.domainWord`."), "internet.domainWord"), 188 | EnumValue("InternetEmailAddress", Some("Represents faker expression `internet.emailAddress`."), "internet.emailAddress"), 189 | EnumValue("InternetImage", Some("Represents faker expression `internet.image`."), "internet.image"), 190 | EnumValue("InternetIpV4Address", Some("Represents faker expression `internet.ipV4Address`."), "internet.ipV4Address"), 191 | EnumValue("InternetIpV4Cidr", Some("Represents faker expression `internet.ipV4Cidr`."), "internet.ipV4Cidr"), 192 | EnumValue("InternetIpV6Address", Some("Represents faker expression `internet.ipV6Address`."), "internet.ipV6Address"), 193 | EnumValue("InternetIpV6Cidr", Some("Represents faker expression `internet.ipV6Cidr`."), "internet.ipV6Cidr"), 194 | EnumValue("InternetMacAddress", Some("Represents faker expression `internet.macAddress`."), "internet.macAddress"), 195 | EnumValue("InternetPassword", Some("Represents faker expression `internet.password`."), "internet.password"), 196 | EnumValue("InternetPrivateIpV4Address", Some("Represents faker expression `internet.privateIpV4Address`."), "internet.privateIpV4Address"), 197 | EnumValue("InternetPublicIpV4Address", Some("Represents faker expression `internet.publicIpV4Address`."), "internet.publicIpV4Address"), 198 | EnumValue("InternetSafeEmailAddress", Some("Represents faker expression `internet.safeEmailAddress`."), "internet.safeEmailAddress"), 199 | EnumValue("InternetSlug", Some("Represents faker expression `internet.slug`."), "internet.slug"), 200 | EnumValue("InternetUrl", Some("Represents faker expression `internet.url`."), "internet.url"), 201 | EnumValue("InternetUuid", Some("Represents faker expression `internet.uuid`."), "internet.uuid"), 202 | EnumValue("JobField", Some("Represents faker expression `job.field`."), "job.field"), 203 | EnumValue("JobKeySkills", Some("Represents faker expression `job.keySkills`."), "job.keySkills"), 204 | EnumValue("JobPosition", Some("Represents faker expression `job.position`."), "job.position"), 205 | EnumValue("JobSeniority", Some("Represents faker expression `job.seniority`."), "job.seniority"), 206 | EnumValue("JobTitle", Some("Represents faker expression `job.title`."), "job.title"), 207 | EnumValue("LeagueOfLegendsChampion", Some("Represents faker expression `leagueOfLegends.champion`."), "leagueOfLegends.champion"), 208 | EnumValue("LeagueOfLegendsLocation", Some("Represents faker expression `leagueOfLegends.location`."), "leagueOfLegends.location"), 209 | EnumValue("LeagueOfLegendsMasteries", Some("Represents faker expression `leagueOfLegends.masteries`."), "leagueOfLegends.masteries"), 210 | EnumValue("LeagueOfLegendsQuote", Some("Represents faker expression `leagueOfLegends.quote`."), "leagueOfLegends.quote"), 211 | EnumValue("LeagueOfLegendsRank", Some("Represents faker expression `leagueOfLegends.rank`."), "leagueOfLegends.rank"), 212 | EnumValue("LeagueOfLegendsSummonerSpell", Some("Represents faker expression `leagueOfLegends.summonerSpell`."), "leagueOfLegends.summonerSpell"), 213 | EnumValue("LebowskiActor", Some("Represents faker expression `lebowski.actor`."), "lebowski.actor"), 214 | EnumValue("LebowskiCharacter", Some("Represents faker expression `lebowski.character`."), "lebowski.character"), 215 | EnumValue("LebowskiQuote", Some("Represents faker expression `lebowski.quote`."), "lebowski.quote"), 216 | EnumValue("LordOfTheRingsCharacter", Some("Represents faker expression `lordOfTheRings.character`."), "lordOfTheRings.character"), 217 | EnumValue("LordOfTheRingsLocation", Some("Represents faker expression `lordOfTheRings.location`."), "lordOfTheRings.location"), 218 | EnumValue("LoremCharacter", Some("Represents faker expression `lorem.character`."), "lorem.character"), 219 | EnumValue("LoremCharacters", Some("Represents faker expression `lorem.characters`."), "lorem.characters"), 220 | EnumValue("LoremParagraph", Some("Represents faker expression `lorem.paragraph`."), "lorem.paragraph"), 221 | EnumValue("LoremSentence", Some("Represents faker expression `lorem.sentence`."), "lorem.sentence"), 222 | EnumValue("LoremWord", Some("Represents faker expression `lorem.word`."), "lorem.word"), 223 | EnumValue("MatzQuote", Some("Represents faker expression `matz.quote`."), "matz.quote"), 224 | EnumValue("MusicChord", Some("Represents faker expression `music.chord`."), "music.chord"), 225 | EnumValue("MusicInstrument", Some("Represents faker expression `music.instrument`."), "music.instrument"), 226 | EnumValue("MusicKey", Some("Represents faker expression `music.key`."), "music.key"), 227 | EnumValue("NameFirstName", Some("Represents faker expression `name.firstName`."), "name.firstName"), 228 | EnumValue("NameFullName", Some("Represents faker expression `name.fullName`."), "name.fullName"), 229 | EnumValue("NameLastName", Some("Represents faker expression `name.lastName`."), "name.lastName"), 230 | EnumValue("NameName", Some("Represents faker expression `name.name`."), "name.name"), 231 | EnumValue("NameNameWithMiddle", Some("Represents faker expression `name.nameWithMiddle`."), "name.nameWithMiddle"), 232 | EnumValue("NamePrefix", Some("Represents faker expression `name.prefix`."), "name.prefix"), 233 | EnumValue("NameSuffix", Some("Represents faker expression `name.suffix`."), "name.suffix"), 234 | EnumValue("NameTitle", Some("Represents faker expression `name.title`."), "name.title"), 235 | EnumValue("NameUsername", Some("Represents faker expression `name.username`."), "name.username"), 236 | EnumValue("NumberDigit", Some("Represents faker expression `number.digit`."), "number.digit"), 237 | EnumValue("NumberRandomDigit", Some("Represents faker expression `number.randomDigit`."), "number.randomDigit"), 238 | EnumValue("NumberRandomDigitNotZero", Some("Represents faker expression `number.randomDigitNotZero`."), "number.randomDigitNotZero"), 239 | EnumValue("NumberRandomNumber", Some("Represents faker expression `number.randomNumber`."), "number.randomNumber"), 240 | EnumValue("OverwatchHero", Some("Represents faker expression `overwatch.hero`."), "overwatch.hero"), 241 | EnumValue("OverwatchLocation", Some("Represents faker expression `overwatch.location`."), "overwatch.location"), 242 | EnumValue("OverwatchQuote", Some("Represents faker expression `overwatch.quote`."), "overwatch.quote"), 243 | EnumValue("PhoneNumberCellPhone", Some("Represents faker expression `phoneNumber.cellPhone`."), "phoneNumber.cellPhone"), 244 | EnumValue("PhoneNumberPhoneNumber", Some("Represents faker expression `phoneNumber.phoneNumber`."), "phoneNumber.phoneNumber"), 245 | EnumValue("PokemonLocation", Some("Represents faker expression `pokemon.location`."), "pokemon.location"), 246 | EnumValue("PokemonName", Some("Represents faker expression `pokemon.name`."), "pokemon.name"), 247 | EnumValue("RandomNextDouble", Some("Represents faker expression `random.nextDouble`."), "random.nextDouble"), 248 | EnumValue("RandomNextLong", Some("Represents faker expression `random.nextLong`."), "random.nextLong"), 249 | EnumValue("RickAndMortyCharacter", Some("Represents faker expression `rickAndMorty.character`."), "rickAndMorty.character"), 250 | EnumValue("RickAndMortyLocation", Some("Represents faker expression `rickAndMorty.location`."), "rickAndMorty.location"), 251 | EnumValue("RickAndMortyQuote", Some("Represents faker expression `rickAndMorty.quote`."), "rickAndMorty.quote"), 252 | EnumValue("RobinQuote", Some("Represents faker expression `robin.quote`."), "robin.quote"), 253 | EnumValue("RockBandName", Some("Represents faker expression `rockBand.name`."), "rockBand.name"), 254 | EnumValue("ShakespeareAsYouLikeItQuote", Some("Represents faker expression `shakespeare.asYouLikeItQuote`."), "shakespeare.asYouLikeItQuote"), 255 | EnumValue("ShakespeareHamletQuote", Some("Represents faker expression `shakespeare.hamletQuote`."), "shakespeare.hamletQuote"), 256 | EnumValue("ShakespeareKingRichardIIIQuote", Some("Represents faker expression `shakespeare.kingRichardIIIQuote`."), "shakespeare.kingRichardIIIQuote"), 257 | EnumValue("ShakespeareRomeoAndJulietQuote", Some("Represents faker expression `shakespeare.romeoAndJulietQuote`."), "shakespeare.romeoAndJulietQuote"), 258 | EnumValue("SlackEmojiActivity", Some("Represents faker expression `slackEmoji.activity`."), "slackEmoji.activity"), 259 | EnumValue("SlackEmojiCelebration", Some("Represents faker expression `slackEmoji.celebration`."), "slackEmoji.celebration"), 260 | EnumValue("SlackEmojiCustom", Some("Represents faker expression `slackEmoji.custom`."), "slackEmoji.custom"), 261 | EnumValue("SlackEmojiEmoji", Some("Represents faker expression `slackEmoji.emoji`."), "slackEmoji.emoji"), 262 | EnumValue("SlackEmojiFoodAndDrink", Some("Represents faker expression `slackEmoji.foodAndDrink`."), "slackEmoji.foodAndDrink"), 263 | EnumValue("SlackEmojiNature", Some("Represents faker expression `slackEmoji.nature`."), "slackEmoji.nature"), 264 | EnumValue("SlackEmojiObjectsAndSymbols", Some("Represents faker expression `slackEmoji.objectsAndSymbols`."), "slackEmoji.objectsAndSymbols"), 265 | EnumValue("SlackEmojiPeople", Some("Represents faker expression `slackEmoji.people`."), "slackEmoji.people"), 266 | EnumValue("SlackEmojiTravelAndPlaces", Some("Represents faker expression `slackEmoji.travelAndPlaces`."), "slackEmoji.travelAndPlaces"), 267 | EnumValue("SpaceAgency", Some("Represents faker expression `space.agency`."), "space.agency"), 268 | EnumValue("SpaceAgencyAbbreviation", Some("Represents faker expression `space.agencyAbbreviation`."), "space.agencyAbbreviation"), 269 | EnumValue("SpaceCompany", Some("Represents faker expression `space.company`."), "space.company"), 270 | EnumValue("SpaceConstellation", Some("Represents faker expression `space.constellation`."), "space.constellation"), 271 | EnumValue("SpaceDistanceMeasurement", Some("Represents faker expression `space.distanceMeasurement`."), "space.distanceMeasurement"), 272 | EnumValue("SpaceGalaxy", Some("Represents faker expression `space.galaxy`."), "space.galaxy"), 273 | EnumValue("SpaceMeteorite", Some("Represents faker expression `space.meteorite`."), "space.meteorite"), 274 | EnumValue("SpaceMoon", Some("Represents faker expression `space.moon`."), "space.moon"), 275 | EnumValue("SpaceNasaSpaceCraft", Some("Represents faker expression `space.nasaSpaceCraft`."), "space.nasaSpaceCraft"), 276 | EnumValue("SpaceNebula", Some("Represents faker expression `space.nebula`."), "space.nebula"), 277 | EnumValue("SpacePlanet", Some("Represents faker expression `space.planet`."), "space.planet"), 278 | EnumValue("SpaceStar", Some("Represents faker expression `space.star`."), "space.star"), 279 | EnumValue("SpaceStarCluster", Some("Represents faker expression `space.starCluster`."), "space.starCluster"), 280 | EnumValue("StarTrekCharacter", Some("Represents faker expression `starTrek.character`."), "starTrek.character"), 281 | EnumValue("StarTrekLocation", Some("Represents faker expression `starTrek.location`."), "starTrek.location"), 282 | EnumValue("StarTrekSpecie", Some("Represents faker expression `starTrek.specie`."), "starTrek.specie"), 283 | EnumValue("StarTrekVillain", Some("Represents faker expression `starTrek.villain`."), "starTrek.villain"), 284 | EnumValue("StockNsdqSymbol", Some("Represents faker expression `stock.nsdqSymbol`."), "stock.nsdqSymbol"), 285 | EnumValue("StockNyseSymbol", Some("Represents faker expression `stock.nyseSymbol`."), "stock.nyseSymbol"), 286 | EnumValue("SuperheroDescriptor", Some("Represents faker expression `superhero.descriptor`."), "superhero.descriptor"), 287 | EnumValue("SuperheroName", Some("Represents faker expression `superhero.name`."), "superhero.name"), 288 | EnumValue("SuperheroPower", Some("Represents faker expression `superhero.power`."), "superhero.power"), 289 | EnumValue("SuperheroPrefix", Some("Represents faker expression `superhero.prefix`."), "superhero.prefix"), 290 | EnumValue("SuperheroSuffix", Some("Represents faker expression `superhero.suffix`."), "superhero.suffix"), 291 | EnumValue("TeamCreature", Some("Represents faker expression `team.creature`."), "team.creature"), 292 | EnumValue("TeamName", Some("Represents faker expression `team.name`."), "team.name"), 293 | EnumValue("TeamSport", Some("Represents faker expression `team.sport`."), "team.sport"), 294 | EnumValue("TeamState", Some("Represents faker expression `team.state`."), "team.state"), 295 | EnumValue("TwinPeaksCharacter", Some("Represents faker expression `twinPeaks.character`."), "twinPeaks.character"), 296 | EnumValue("TwinPeaksLocation", Some("Represents faker expression `twinPeaks.location`."), "twinPeaks.location"), 297 | EnumValue("TwinPeaksQuote", Some("Represents faker expression `twinPeaks.quote`."), "twinPeaks.quote"), 298 | EnumValue("UniversityName", Some("Represents faker expression `university.name`."), "university.name"), 299 | EnumValue("UniversityPrefix", Some("Represents faker expression `university.prefix`."), "university.prefix"), 300 | EnumValue("UniversitySuffix", Some("Represents faker expression `university.suffix`."), "university.suffix"), 301 | EnumValue("WeatherDescription", Some("Represents faker expression `weather.description`."), "weather.description"), 302 | EnumValue("WeatherTemperatureCelsius", Some("Represents faker expression `weather.temperatureCelsius`."), "weather.temperatureCelsius"), 303 | EnumValue("WeatherTemperatureFahrenheit", Some("Represents faker expression `weather.temperatureFahrenheit`."), "weather.temperatureFahrenheit"), 304 | EnumValue("WitcherCharacter", Some("Represents faker expression `witcher.character`."), "witcher.character"), 305 | EnumValue("WitcherLocation", Some("Represents faker expression `witcher.location`."), "witcher.location"), 306 | EnumValue("WitcherMonster", Some("Represents faker expression `witcher.monster`."), "witcher.monster"), 307 | EnumValue("WitcherQuote", Some("Represents faker expression `witcher.quote`."), "witcher.quote"), 308 | EnumValue("WitcherSchool", Some("Represents faker expression `witcher.school`."), "witcher.school"), 309 | EnumValue("WitcherWitcher", Some("Represents faker expression `witcher.witcher`."), "witcher.witcher"), 310 | EnumValue("YodaQuote", Some("Represents faker expression `yoda.quote`."), "yoda.quote"), 311 | EnumValue("ZeldaCharacter", Some("Represents faker expression `zelda.character`."), "zelda.character"), 312 | EnumValue("ZeldaGame", Some("Represents faker expression `zelda.game`."), "zelda.game") 313 | )) 314 | 315 | object Args { 316 | val Expr = Argument("expr", OptionInputType(StringType)) 317 | val Type = Argument("type", OptionInputType(FakeType)) 318 | val Locale = Argument("locale", OptionInputType(StringType)) 319 | val Seed = Argument("seed", OptionInputType(IntType)) 320 | val MinElems = Argument("min", OptionInputType(IntType)) 321 | val MaxElems = Argument("max", OptionInputType(IntType)) 322 | val Past = Argument("past", OptionInputType(BooleanType)) 323 | val Future = Argument("future", OptionInputType(BooleanType)) 324 | } 325 | 326 | object Dirs { 327 | val Fake = Directive("fake", 328 | arguments = Args.Expr :: Args.Type :: Args.MinElems :: Args.MaxElems :: Args.Past :: Args.Future :: Nil, 329 | locations = Set(DirectiveLocation.FieldDefinition)) 330 | 331 | val FakeConfig = Directive("fakeConfig", 332 | arguments = Args.Locale :: Args.Seed :: Nil, 333 | locations = Set(DirectiveLocation.Schema)) 334 | } 335 | } 336 | 337 | case class FakeValue(expr: Option[String], min: Option[Int], max: Option[Int], past: Option[Boolean], future: Option[Boolean], rnd: Random, faker: Faker) { 338 | private lazy val withoutExpr = copy(expr = None, min = None, max = None) 339 | 340 | def coerce(ctx: Context[GatewayContext, _]): Action[GatewayContext, Any] = { 341 | def coerceType(tpe: OutputType[_]): Any = tpe match { 342 | case OptionType(ofType) ⇒ 343 | if (rnd.nextBoolean()) None 344 | else coerceType(ofType) 345 | 346 | case ListType(ofType) ⇒ 347 | val size = min.getOrElse(0) + rnd.nextInt(max.fold(20)(max ⇒ max - min.getOrElse(0) + 1)) 348 | 349 | (1 to size).toVector.map(_ ⇒ coerceType(ofType)) 350 | 351 | case s: ScalarAlias[_, _] ⇒ 352 | coerceType(s.aliasFor) 353 | 354 | case s: ScalarType[_] if s.name == StringType.name ⇒ 355 | expr match { 356 | case Some(e) ⇒ resolveExpr(e) 357 | case None ⇒ faker.lorem().paragraph() 358 | } 359 | 360 | case s: ScalarType[_] if s.name == IntType.name ⇒ 361 | expr match { 362 | case Some(e) ⇒ resolveExpr(e).toInt 363 | case None ⇒ rnd.nextInt() 364 | } 365 | 366 | case s: ScalarType[_] if s.name == LongType.name ⇒ 367 | expr match { 368 | case Some(e) ⇒ resolveExpr(e).toLong 369 | case None ⇒ rnd.nextLong() 370 | } 371 | 372 | case s: ScalarType[_] if s.name == BigIntType.name ⇒ 373 | expr match { 374 | case Some(e) ⇒ BigInt(resolveExpr(e)) 375 | case None ⇒ BigInt(rnd.nextLong()) 376 | } 377 | 378 | case s: ScalarType[_] if s.name == BigDecimalType.name ⇒ 379 | expr match { 380 | case Some(e) ⇒ BigDecimal(resolveExpr(e)) 381 | case None ⇒ BigDecimal(rnd.nextDouble()) 382 | } 383 | 384 | case s: ScalarType[_] if s.name == IDType.name ⇒ 385 | expr match { 386 | case Some(e) ⇒ resolveExpr(e) 387 | case None ⇒ "" + rnd.nextLong() 388 | } 389 | 390 | case s: ScalarType[_] if s.name == FloatType.name ⇒ 391 | expr match { 392 | case Some(e) ⇒ resolveExpr(e).toDouble 393 | case None ⇒ rnd.nextDouble() 394 | } 395 | 396 | case s: ScalarType[_] if s.name == BooleanType.name ⇒ 397 | expr match { 398 | case Some(e) ⇒ resolveExpr(e).toBoolean 399 | case None ⇒ rnd.nextBoolean() 400 | } 401 | 402 | case s: ScalarType[_] if s.name == CustomScalars.DateTimeType.name ⇒ 403 | resolveDate 404 | 405 | case e: EnumType[_] ⇒ 406 | e.values(rnd.nextInt(e.values.size)).value 407 | 408 | case s: CompositeType[_] ⇒ 409 | withoutExpr 410 | 411 | case t ⇒ 412 | throw new IllegalStateException(s"Can't fake type ${SchemaRenderer.renderTypeName(t)} just yet.") 413 | } 414 | 415 | coerceType(ctx.field.fieldType) 416 | } 417 | 418 | def resolveExpr(e: String) = { 419 | if (FakeValue.ExpressionPattern.matcher(e).find()) 420 | faker.expression(e) 421 | else 422 | faker.expression("#{" + e + "}") 423 | } 424 | 425 | def resolveDate = { 426 | val isPast = 427 | (past, future) match { 428 | case (Some(p), Some(f)) ⇒ p 429 | case (Some(p), None) ⇒ p 430 | case (None, Some(f)) ⇒ !f 431 | case (None, None) ⇒ rnd.nextBoolean() 432 | } 433 | 434 | val date = 435 | if (isPast) 436 | faker.date().past(700, TimeUnit.DAYS) 437 | else 438 | faker.date().future(700, TimeUnit.DAYS) 439 | 440 | ZonedDateTime.ofInstant(date.toInstant, ZoneId.of("UTC")) 441 | } 442 | } 443 | 444 | object FakeValue { 445 | val ExpressionPattern = Pattern.compile("#\\{([a-z0-9A-Z_.]+)\\s?(?:'([^']+)')?(?:,'([^']+)')*\\}") 446 | } 447 | 448 | 449 | 450 | 451 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/schema/materializer/directive/GraphQLDirectiveProvider.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.schema.materializer.directive 2 | 3 | import io.circe.Json 4 | import sangria.gateway.schema.materializer.{GatewayContext, GraphQLIncludedSchema} 5 | import sangria.schema._ 6 | import sangria.macros.derive._ 7 | import sangria.ast 8 | import sangria.ast.AstVisitor 9 | import sangria.gateway.schema.CustomScalars 10 | import sangria.gateway.schema.materializer.directive.HttpDirectiveProvider.Args.{HeaderType, QueryParamType} 11 | import sangria.marshalling.queryAst.queryAstInputUnmarshaller 12 | import sangria.validation.TypeInfo 13 | import sangria.visitor.VisitorCommand 14 | import sangria.introspection.TypeNameMetaField 15 | import sangria.marshalling.circe._ 16 | import io.circe.generic.auto._ 17 | 18 | import scala.collection.mutable 19 | import scala.concurrent.ExecutionContext 20 | 21 | class GraphQLDirectiveProvider(implicit ec: ExecutionContext) extends DirectiveProvider { 22 | import GraphQLDirectiveProvider._ 23 | 24 | private val TypeNameField = ast.Field(None, TypeNameMetaField.name, Vector.empty, Vector.empty, Vector.empty) 25 | 26 | def resolvers(ctx: GatewayContext) = Seq( 27 | AdditionalDirectives(Seq(Dirs.IncludeSchema)), 28 | AdditionalTypes(ctx.allTypes.toList), 29 | 30 | ExistingFieldResolver { 31 | case (o: GraphQLIncludedSchema, _, f) if ctx.graphqlIncludes.exists(_.include.name == o.include.name) && f.astDirectives.exists(_.name == "delegate") ⇒ 32 | val schema = ctx.graphqlIncludes.find(_.include.name == o.include.name).get 33 | 34 | c ⇒ { 35 | val (updatedFields, fragments, vars) = prepareOriginFields(o, c.query, c.schema, c.astFields, c.parentType) 36 | val varDefs = vars.toVector.flatMap(v ⇒ c.query.operation(c.ctx.operationName).get.variables.find(_.name == v)) 37 | val queryOp = ast.OperationDefinition(ast.OperationType.Query, 38 | name = Some("DelegatedQuery"), 39 | variables = varDefs, 40 | selections = updatedFields) 41 | val query = ast.Document(queryOp +: fragments) 42 | 43 | ctx.request(c, schema, query, c.ctx.queryVars, c.astFields.head.outputName).map(value ⇒ 44 | ResolverBasedAstSchemaBuilder.extractValue[Json](c.field.fieldType, Some(value))) 45 | } 46 | }, 47 | 48 | ExistingScalarResolver { 49 | case ctx if 50 | ctx.origin.isInstanceOf[GraphQLIncludedSchema] || 51 | ctx.existing.name != CustomScalars.DateTimeType.name ⇒ 52 | 53 | ctx.existing.copy( 54 | coerceUserInput = Right(_), 55 | coerceOutput = (v, _) ⇒ v, 56 | coerceInput = v ⇒ Right(queryAstInputUnmarshaller.getScalaScalarValue(v))) 57 | }, 58 | 59 | // Current behaviour: for all conflicting types, only one would be picked - 60 | // we assume that their structure and semantics are the same. 61 | // Potential future improvement: provide more flexible, directive-based approach to resolve the name conflicts 62 | ConflictResolver((origin, conflictingTypes) ⇒ 63 | conflictingTypes 64 | .collectFirst{case opt: BuiltMaterializedTypeInst ⇒ opt} 65 | .getOrElse(conflictingTypes.head)), 66 | 67 | DirectiveFieldProvider(Dirs.IncludeFields, _.withArgs(Args.Schema, Args.Type, Args.Fields, Args.Excludes)((schema, tpe, fields, excludes) ⇒ 68 | ctx.findFields(schema, tpe, fields, excludes)))) 69 | 70 | private def prepareOriginFields(origin: GraphQLIncludedSchema, query: ast.Document, schema: Schema[GatewayContext, _], queryFields: Vector[ast.Field], parentType: OutputType[_]) = 71 | queryFields.foldLeft((Vector.empty[ast.Field], Vector.empty[ast.FragmentDefinition], Set.empty[String])) { 72 | case ((fieldAcc, fragAcc, varAcc), f) ⇒ 73 | val (ufield, ufrag, uvars) = prepareAst(f, origin, query, schema, Some(parentType), fragAcc.map(_.name).toSet) 74 | 75 | (fieldAcc :+ ufield, fragAcc ++ ufrag, varAcc ++ uvars) 76 | } 77 | 78 | private def prepareAst[N <: ast.AstNode]( 79 | node: N, 80 | origin: GraphQLIncludedSchema, 81 | query: ast.Document, 82 | schema: Schema[GatewayContext, _], 83 | parentType: Option[OutputType[_]], 84 | seenFragments: Set[String] 85 | ): (N, Vector[ast.FragmentDefinition], Set[String]) = { 86 | val fragments = mutable.HashSet[String]() 87 | val vars = mutable.HashSet[String]() 88 | 89 | val updatedNode = 90 | AstVisitor.visitAstWithTypeInfo(schema, node) { typeInfo ⇒ 91 | parentType foreach typeInfo.forcePushType 92 | 93 | AstVisitor( 94 | onEnter = { 95 | case field: ast.Field ⇒ 96 | typeInfo.previousParentType match { 97 | case Some(parent) ⇒ 98 | origin.schema.outputTypes.get(parent.name) match { 99 | case Some(originType: ObjectLikeType[_, _]) if !originType.fieldsByName.contains(field.name) ⇒ 100 | // Field does not belong to an origin - no delegation 101 | VisitorCommand.Delete 102 | 103 | case _ ⇒ VisitorCommand.Continue 104 | } 105 | 106 | case None ⇒ VisitorCommand.Continue 107 | } 108 | 109 | case v: ast.VariableValue ⇒ 110 | vars += v.name 111 | VisitorCommand.Continue 112 | 113 | case f: ast.FragmentSpread ⇒ 114 | fragments += f.name 115 | VisitorCommand.Continue 116 | }, 117 | onLeave = { 118 | case field: ast.Field ⇒ 119 | typeInfo.parentType match { 120 | case Some(parent) ⇒ 121 | origin.schema.outputTypes.get(parent.name) match { 122 | case Some(originType: ObjectLikeType[_, _]) if originType.fieldsByName contains field.name ⇒ 123 | injectTypeName(origin, field, parent, typeInfo) 124 | 125 | case _ ⇒ VisitorCommand.Continue 126 | } 127 | 128 | case None ⇒ VisitorCommand.Continue 129 | } 130 | }) 131 | } 132 | 133 | val fragmentsToUpdate = fragments.toVector.filterNot(name ⇒ seenFragments.contains(name)) 134 | val allSeenFragments = seenFragments ++ fragmentsToUpdate 135 | val updatedFragments = fragmentsToUpdate.map(fn ⇒ prepareAst(query.fragments(fn), origin, query, schema, None, allSeenFragments)) 136 | val allVars = vars.toSet ++ updatedFragments.flatMap(_._3) 137 | val finalFragments = updatedFragments.flatMap(u ⇒ u._1 +: u._2) 138 | 139 | (updatedNode, finalFragments, allVars) 140 | } 141 | 142 | def injectTypeName(origin: GraphQLIncludedSchema, field: ast.Field, parent: CompositeType[_], typeInfo: TypeInfo): VisitorCommand = { 143 | val fieldType = 144 | origin.schema.outputTypes.get(parent.name) match { 145 | case Some(originType: ObjectLikeType[_, _]) ⇒ originType.fieldsByName.get(field.name).map(_.head.fieldType) 146 | case _ ⇒ None 147 | } 148 | 149 | fieldType.map(_.namedType).fold(VisitorCommand.Continue: VisitorCommand) { 150 | case _: AbstractType ⇒ 151 | // Field has abstract result type, so we need to inject `__typename` to make proper instance checks at the Gateway level 152 | VisitorCommand.Transform(field.copy(selections = TypeNameField +: field.selections)) 153 | case _ ⇒ VisitorCommand.Continue 154 | } 155 | } 156 | } 157 | 158 | object GraphQLDirectiveProvider { 159 | object Args { 160 | val OAuthClientCredentialsType = deriveInputObjectType[OAuthClientCredentials]() 161 | 162 | val Name = Argument("name", StringType) 163 | val Url = Argument("url", StringType) 164 | val Headers = Argument("headers", OptionInputType(ListInputType(HeaderType))) 165 | val DelegateHeaders = Argument("delegateHeaders", OptionInputType(ListInputType(StringType)), 166 | "Delegate headers from original gateway request to the downstream services.") 167 | val QueryParams = Argument("query", OptionInputType(ListInputType(QueryParamType))) 168 | val OAuth = Argument("oauth", OptionInputType(OAuthClientCredentialsType)) 169 | 170 | val Schema = Argument("schema", StringType, description = "the name of the schema included with @includeGraphQL") 171 | val Type = Argument("type", StringType, description = "name of the type from the external schema") 172 | val Fields = Argument("fields", OptionInputType(ListInputType(StringType))) 173 | val Excludes = Argument("excludes", OptionInputType(ListInputType(StringType))) 174 | } 175 | 176 | object Dirs { 177 | val IncludeSchema = Directive("includeSchema", 178 | repeatable = true, 179 | arguments = Args.Name :: Args.Url :: Args.Headers :: Args.DelegateHeaders :: Args.QueryParams :: Args.OAuth :: Nil, 180 | locations = Set(DirectiveLocation.Schema)) 181 | 182 | val IncludeFields = Directive("includeFields", 183 | repeatable = true, 184 | arguments = Args.Schema :: Args.Type :: Args.Fields :: Args.Excludes :: Nil, 185 | locations = Set(DirectiveLocation.Object)) 186 | } 187 | } 188 | 189 | case class OAuthClientCredentials(url: String, clientId: String, clientSecret: String, scopes: Seq[String]) -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/schema/materializer/directive/HttpDirectiveProvider.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.schema.materializer.directive 2 | 3 | import language.existentials 4 | 5 | import io.circe.Json 6 | import sangria.gateway.http.client.HttpClient 7 | import sangria.gateway.json.CirceJsonPath 8 | import sangria.gateway.schema.materializer.GatewayContext 9 | import sangria.gateway.schema.materializer.GatewayContext.{convertArgs, namedType} 10 | import sangria.schema.ResolverBasedAstSchemaBuilder.extractValue 11 | import sangria.schema._ 12 | import sangria.marshalling.circe._ 13 | 14 | import scala.concurrent.{ExecutionContext, Future} 15 | 16 | class HttpDirectiveProvider(client: HttpClient)(implicit ec: ExecutionContext) extends DirectiveProvider { 17 | import HttpDirectiveProvider._ 18 | 19 | def resolvers(ctx: GatewayContext) = Seq( 20 | DirectiveResolver(Dirs.HttpGet, 21 | complexity = Some(_ ⇒ (_, _, _) ⇒ 1000.0), 22 | resolve = c ⇒ c.withArgs(Args.Url, Args.Headers, Args.DelegateHeaders, Args.QueryParams, Args.ForAll) { (rawUrl, rawHeaders, delegateHeaders, rawQueryParams, forAll) ⇒ 23 | val args = Some(convertArgs(c.ctx.args, c.ctx.astFields.head)) 24 | 25 | def makeRequest(tpe: OutputType[_], c: Context[GatewayContext, _], args: Option[Json], elem: Json = Json.Null) = { 26 | val somec = Some(c) 27 | val url = GatewayContext.fillPlaceholders(somec, rawUrl, args, elem) 28 | val headers = extractMap(somec, rawHeaders, args, elem) 29 | val query = extractMap(somec, rawQueryParams, args, elem) 30 | val extraHeaders = extractDelegatedHeaders(c, delegateHeaders) 31 | 32 | client.request(HttpClient.Method.Get, url, query, headers ++ extraHeaders).flatMap(GatewayContext.parseJson) 33 | } 34 | 35 | forAll match { 36 | case Some(elem) ⇒ 37 | CirceJsonPath.query(c.ctx.value.asInstanceOf[Json], elem) match { 38 | case json: Json if json.isArray ⇒ 39 | Future.sequence(json.asArray.get.map(e ⇒ makeRequest(namedType(c.ctx.field.fieldType), c.ctx, args, e))) map { v ⇒ 40 | extractValue(c.ctx.field.fieldType, Some(Json.arr(v.asInstanceOf[Seq[Json]]: _*))) 41 | } 42 | case e ⇒ 43 | makeRequest(c.ctx.field.fieldType, c.ctx, args, e) 44 | } 45 | case None ⇒ 46 | makeRequest(c.ctx.field.fieldType, c.ctx, args) 47 | } 48 | })) 49 | } 50 | 51 | object HttpDirectiveProvider { 52 | def extractMap(ctx: Option[Context[GatewayContext, _]], in: Option[Seq[InputObjectType.DefaultInput]], args: Option[Json] = None, elem: Json = Json.Null) = { 53 | in.map(_.map { h ⇒ 54 | val name = h("name").asInstanceOf[String] 55 | val value = h("value").asInstanceOf[String] 56 | 57 | name → GatewayContext.fillPlaceholders(ctx, value, args, elem) 58 | }).getOrElse(Nil) 59 | } 60 | 61 | def extractDelegatedHeaders(ctx: Context[GatewayContext, _], delegateHeaders: Option[Seq[String]]) = 62 | delegateHeaders match { 63 | case Some(hs) ⇒ ctx.ctx.originalHeaders.filter(orig ⇒ hs.contains(orig._1)) 64 | case None ⇒ Seq.empty 65 | } 66 | 67 | object Args { 68 | val HeaderType = InputObjectType("Header", fields = List( 69 | InputField("name", StringType), 70 | InputField("value", StringType))) 71 | 72 | val QueryParamType = InputObjectType("QueryParam", fields = List( 73 | InputField("name", StringType), 74 | InputField("value", StringType))) 75 | 76 | val Url = Argument("url", StringType) 77 | val Headers = Argument("headers", OptionInputType(ListInputType(HeaderType))) 78 | val DelegateHeaders = Argument("delegateHeaders", OptionInputType(ListInputType(StringType)), 79 | "Delegate headers from original gateway request to the downstream services.") 80 | val QueryParams = Argument("query", OptionInputType(ListInputType(QueryParamType))) 81 | val ForAll = Argument("forAll", OptionInputType(StringType)) 82 | } 83 | 84 | object Dirs { 85 | val HttpGet = Directive("httpGet", 86 | arguments = Args.Url :: Args.Headers :: Args.DelegateHeaders :: Args.QueryParams :: Args.ForAll :: Nil, 87 | locations = Set(DirectiveLocation.FieldDefinition)) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/util/LogColors.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.util 2 | 3 | import ch.qos.logback.classic.Level 4 | import ch.qos.logback.classic.spi.ILoggingEvent 5 | import ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase 6 | import ch.qos.logback.core.pattern.color.ANSIConstants._ 7 | 8 | class LogColors extends ForegroundCompositeConverterBase[ILoggingEvent] { 9 | override def getForegroundColorCode(event: ILoggingEvent): String = 10 | event.getLevel.toInt match { 11 | case Level.ERROR_INT ⇒ 12 | BOLD + RED_FG 13 | case Level.WARN_INT ⇒ 14 | YELLOW_FG 15 | case Level.INFO_INT ⇒ 16 | GREEN_FG 17 | case Level.DEBUG_INT ⇒ 18 | CYAN_FG 19 | case _ ⇒ 20 | DEFAULT_FG 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/sangria/gateway/util/Logging.scala: -------------------------------------------------------------------------------- 1 | package sangria.gateway.util 2 | 3 | import com.typesafe.scalalogging.Logger 4 | 5 | trait Logging { 6 | val logger = Logger(this.getClass) 7 | } 8 | -------------------------------------------------------------------------------- /testSchema.graphql: -------------------------------------------------------------------------------- 1 | # It's an example schema: 2 | # 3 | # * Proxies some endpoints from the http://swapi.co 4 | # * Merges and delegates execution for 2 GraphQL external GraphQL API 5 | 6 | extend schema 7 | @includeSchema(name: "starWars", url: "http://try.sangria-graphql.org/graphql") 8 | @includeSchema(name: "db", url: "https://developer.deutschebahn.com/free1bahnql/graphql") 9 | 10 | """ 11 | The root query type. 12 | """ 13 | type Query 14 | @includeFields(schema: "starWars", type: "Query") 15 | @includeFields(schema: "db", type: "Query") { 16 | 17 | "A character from the StarWars (REST API)" 18 | person(id: Int!): Person 19 | @httpGet(url: "https://swapi.co/api/people/${arg.id}/") 20 | 21 | "A list of characters from the StarWars (REST API)" 22 | people(page: Int): [Person] 23 | @httpGet( 24 | url: "http://swapi.co/api/people" 25 | query: {name: "page", value: "${arg.page}"}) 26 | @value(name: "results") 27 | 28 | fruits: [Fruit!]! @fake(min: 5) 29 | } 30 | 31 | type Film { 32 | title: String 33 | } 34 | 35 | type Person { 36 | name: String 37 | address: String! @fake(expr: "#{address.fullAddress}") 38 | birthday: DateTime! @fake(past: true) 39 | size: Int @value(name: "height") 40 | homeworld: Planet @httpGet(url: "${value.homeworld}") 41 | films: [Film] @httpGet(forAll: "$.films", url: "${elem.$}") 42 | } 43 | 44 | "A planet from the StarWars universe" 45 | type Planet { 46 | name: String 47 | } 48 | 49 | type Fruit { 50 | weight: Int! 51 | description: String! @fake(expr: "#{hipster.word}") 52 | color: Color! 53 | } 54 | 55 | enum Color { 56 | RED 57 | BLUE 58 | MAGENTA 59 | } --------------------------------------------------------------------------------