├── .github └── workflows │ ├── lint.yml │ └── ruby.yml ├── .gitignore ├── .rubocop.yml ├── .rubocop_standardrb.yml ├── .rubocop_standardrb_overrides.yml ├── .rubocop_todo.yml ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── Gemfile ├── LICENSE.md ├── README.md ├── Rakefile ├── bin └── prmd ├── docs └── schemata.md ├── lib ├── prmd.rb └── prmd │ ├── cli.rb │ ├── cli │ ├── base.rb │ ├── combine.rb │ ├── doc.rb │ ├── generate.rb │ ├── render.rb │ ├── stub.rb │ └── verify.rb │ ├── commands.rb │ ├── commands │ ├── combine.rb │ ├── init.rb │ ├── render.rb │ └── verify.rb │ ├── core │ ├── combiner.rb │ ├── generator.rb │ ├── reference_localizer.rb │ ├── renderer.rb │ └── schema_hash.rb │ ├── core_ext │ └── optparse.rb │ ├── hash_helpers.rb │ ├── link.rb │ ├── load_schema_file.rb │ ├── multi_loader.rb │ ├── multi_loader │ ├── json.rb │ ├── loader.rb │ ├── toml.rb │ ├── yajl.rb │ ├── yaml.rb │ └── yml.rb │ ├── rake_tasks │ ├── base.rb │ ├── combine.rb │ ├── doc.rb │ └── verify.rb │ ├── schema.rb │ ├── template.rb │ ├── templates │ ├── combine_head.json │ ├── init_default.json │ ├── init_resource.json.erb │ ├── link_schema_properties.md.erb │ ├── schema.erb │ ├── schemata.md.erb │ ├── schemata │ │ ├── helper.erb │ │ ├── link.md.erb │ │ └── link_curl_example.md.erb │ └── table_of_contents.erb │ ├── url_generator.rb │ ├── url_generators │ └── generators │ │ ├── default.rb │ │ └── json.rb │ ├── utils.rb │ └── version.rb ├── prmd.gemspec ├── schemas ├── hyper-schema.json ├── interagent-hyper-schema.json └── schema.json └── test ├── cli ├── combine_test.rb ├── doc_test.rb ├── generate_test.rb ├── render_test.rb └── verify_test.rb ├── commands ├── combine_test.rb ├── init_test.rb ├── render_test.rb └── verify_test.rb ├── core └── reference_localizer_test.rb ├── helpers.rb ├── link_test.rb ├── multi_loader ├── common.rb ├── json_test.rb ├── toml_test.rb ├── yajl_test.rb └── yaml_test.rb ├── rake_tasks ├── combine_test.rb ├── doc_test.rb └── verify_test.rb ├── schema_test.rb ├── schemata ├── data │ ├── test.json │ ├── test.toml │ └── test.yaml └── input │ ├── doc-settings.json │ ├── meta.json │ ├── rake-meta.json │ ├── rake_combine │ ├── post.json │ └── user.json │ ├── rake_doc.json │ ├── rake_verify.json │ └── user.json └── utils_test.rb /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened] 9 | 10 | permissions: 11 | checks: write 12 | contents: read 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | ruby-version: ['3.2', '3.3', '3.4'] 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby-version }} 27 | bundler-cache: true 28 | - uses: wearerequired/lint-action@v2 29 | with: 30 | auto_fix: false 31 | rubocop: true 32 | rubocop_auto_fix: false 33 | rubocop_command_prefix: bundle exec 34 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | name: "Ruby ${{ matrix.ruby-version }}" 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | ruby-version: ['3.2', '3.3', '3.4'] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Ruby 19 | uses: ruby/setup-ruby@v1 20 | with: 21 | ruby-version: ${{ matrix.ruby-version }} 22 | bundler-cache: true 23 | - name: Run tests 24 | run: bundle exec rake 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .ruby-version 4 | .bundle 5 | .config 6 | coverage 7 | InstalledFiles 8 | lib/bundler/man 9 | pkg 10 | rdoc 11 | spec/reports 12 | test/tmp 13 | test/version_tmp 14 | tmp 15 | 16 | # YARD artifacts 17 | .yardoc 18 | _yardoc 19 | doc/ 20 | Gemfile.lock 21 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | inherit_from: 4 | - .rubocop_standardrb.yml 5 | - .rubocop_standardrb_overrides.yml 6 | - .rubocop_todo.yml 7 | 8 | AllCops: 9 | NewCops: disable 10 | Exclude: 11 | - vendor/**/* 12 | -------------------------------------------------------------------------------- /.rubocop_standardrb_overrides.yml: -------------------------------------------------------------------------------- 1 | Layout/SpaceInsideHashLiteralBraces: 2 | Enabled: true 3 | EnforcedStyle: space 4 | EnforcedStyleForEmptyBraces: no_space 5 | 6 | Style/FrozenStringLiteralComment: 7 | Enabled: true 8 | 9 | Style/TrailingCommaInArguments: 10 | Enabled: true 11 | EnforcedStyleForMultiline: consistent_comma 12 | 13 | Style/TrailingCommaInArrayLiteral: 14 | Enabled: true 15 | EnforcedStyleForMultiline: consistent_comma 16 | 17 | Style/TrailingCommaInHashLiteral: 18 | Enabled: true 19 | EnforcedStyleForMultiline: consistent_comma 20 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2025-02-06 15:17:15 UTC using RuboCop version 1.71.2. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # This cop supports unsafe autocorrection (--autocorrect-all). 11 | # Configuration parameters: AllowSafeAssignment. 12 | Lint/AssignmentInCondition: 13 | Exclude: 14 | - 'lib/prmd/core/combiner.rb' 15 | 16 | Security/YAMLLoad: 17 | Exclude: 18 | - 'lib/prmd/multi_loader/yaml.rb' 19 | 20 | # Offense count: 1 21 | Naming/ConstantName: 22 | Exclude: 23 | - 'lib/prmd/schema.rb' 24 | 25 | # Offense count: 2 26 | # This cop supports unsafe autocorrection (--autocorrect-all). 27 | Security/JSONLoad: 28 | Exclude: 29 | - 'lib/prmd/cli/base.rb' 30 | - 'lib/prmd/multi_loader/json.rb' 31 | 32 | # Offense count: 67 33 | # This cop supports unsafe autocorrection (--autocorrect-all). 34 | # Configuration parameters: EnforcedStyle. 35 | # SupportedStyles: always, always_true, never 36 | Style/FrozenStringLiteralComment: 37 | Enabled: false 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Getting Involved 2 | 3 | New contributors are always welcome, when it doubt please ask questions. We strive to be an open and welcoming community. Please be nice to one another. 4 | 5 | ### Coding 6 | 7 | * Pick a task: 8 | * Offer feedback on open [pull requests](https://github.com/interagent/prmd/pulls). 9 | * Review open [issues](https://github.com/interagent/prmd/issues) for things to help on. 10 | * [Create an issue](https://github.com/interagent/prmd/issues/new) to start a discussion on additions or features. 11 | * Fork the project, add your changes and tests to cover them in a topic branch. 12 | * Commit your changes and rebase against `interagent/prmd` to ensure everything is up to date. 13 | * [Submit a pull request](https://github.com/interagent/prmd/compare/). 14 | 15 | ### Non-Coding 16 | 17 | * Offer feedback on open [issues](https://github.com/interagent/prmd/issues). 18 | * Organize or volunteer at events. 19 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | * Adel Qalieh 2 | * Alex Fedorov 3 | * Alex Sergeyev 4 | * Alex Sulim 5 | * Alexey Fedorov 6 | * Arnold 7 | * Blake Visin 8 | * Brandur 9 | * Brandur 10 | * Brian McManus 11 | * Camilo Aguilar 12 | * Cesar Andreu 13 | * Chris Continanza 14 | * Corey Powell 15 | * Dan Peterson 16 | * Dane Harrigan 17 | * Dominik Honnef 18 | * Eric J. Holmes 19 | * Ernesto Jiménez 20 | * Glenn Goodrich 21 | * Guillaume Coderre 22 | * Harry Maclean 23 | * Jason Rudolph 24 | * Juan Pablo Buritica 25 | * Justin Halsall 26 | * Kousuke Ebihara 27 | * Kousuke Ebihara 28 | * Kyle Rames 29 | * Lucas Carvalho 30 | * Mark McGranaghan 31 | * Martin Seeler 32 | * Masaya Myojin 33 | * Mathias Jean Johansen 34 | * Matt Gauger 35 | * Matthew Conway 36 | * Matthew Tylee Atkinson 37 | * Michael Sauter 38 | * Nate Smith 39 | * Nikolay Markov 40 | * Olivier Lance 41 | * Reese 42 | * Reese Wilson 43 | * Scott Clasen 44 | * Scott Moak 45 | * Takehiro Adachi 46 | * Timothée Peignier 47 | * Wesley Beary 48 | * Wesley Beary 49 | * Willem Dekker 50 | * Zack Shapiro 51 | * geemus 52 | * yuemori -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in prmd.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2016 [CONTRIBUTORS.md](https://github.com/interagent/prmd/blob/main/CONTRIBUTORS.md) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prmd 2 | 3 | [![Gem Version](https://badge.fury.io/rb/prmd.svg)](http://badge.fury.io/rb/prmd) 4 | 5 | JSON Schema tooling: scaffold, verify, and generate documentation 6 | from JSON Schema documents. 7 | 8 | 9 | ## Introduction 10 | 11 | [JSON Schema](http://json-schema.org/) provides a great way to describe 12 | an API. prmd provides tools for bootstrapping a description like this, 13 | verifying its completeness, and generating documentation from the 14 | specification. 15 | 16 | To learn more about JSON Schema in general, start with 17 | [this excellent guide](http://spacetelescope.github.io/understanding-json-schema/) 18 | and supplement with the [specification](http://json-schema.org/documentation.html). 19 | The JSON Schema usage conventions expected by prmd specifically are 20 | described in [/docs/schemata.md](/docs/schemata.md). 21 | 22 | ## Installation 23 | 24 | Install the command-line tool with: 25 | 26 | ```bash 27 | $ gem install prmd 28 | ``` 29 | 30 | If you're using prmd within a Ruby project, you may want to add it 31 | to the application's Gemfile: 32 | 33 | ```ruby 34 | gem 'prmd' 35 | ``` 36 | 37 | ```bash 38 | $ bundle install 39 | ``` 40 | 41 | ## Usage 42 | 43 | Prmd provides four main commands: 44 | 45 | * `init`: Scaffold resource schemata 46 | * `combine`: Combine schemata and metadata into single schema 47 | * `verify`: Verify a schema 48 | * `doc`: Generate documentation from a schema 49 | * `render`: Render views from schema 50 | 51 | Here's an example of using these commands in a typical workflow: 52 | 53 | ```bash 54 | # Fill out the resource schemata 55 | $ mkdir -p schemata 56 | $ prmd init app > schemata/app.json 57 | $ prmd init user > schemata/user.json 58 | $ vim schemata/{app,user}.json # edit scaffolded files 59 | 60 | # Provide top-level metadata 61 | $ cat < meta.json 62 | { 63 | "description": "Hello world prmd API", 64 | "id": "hello-prmd", 65 | "links": [{ 66 | "href": "https://api.hello.com", 67 | "rel": "self" 68 | }], 69 | "title": "Hello Prmd" 70 | } 71 | EOF 72 | 73 | # Combine into a single schema 74 | $ prmd combine --meta meta.json schemata/ > schema.json 75 | 76 | # Check it’s all good 77 | $ prmd verify schema.json 78 | 79 | # Build docs 80 | $ prmd doc schema.json > schema.md 81 | ``` 82 | 83 | ### Using YAML instead of JSON as a resource and meta format 84 | 85 | `init` and `combine` supports YAML format: 86 | 87 | ```bash 88 | # Generate resources in YAML format 89 | $ prmd init --yaml app > schemata/app.yml 90 | $ prmd init --yaml user > schemata/user.yml 91 | 92 | # Combine into a single schema 93 | $ prmd combine --meta meta.json schemata/ > schema.json 94 | ``` 95 | 96 | `combine` can detect both `*.yml` and `*.json` and use them side by side. For example, if one have a lot of legacy JSON resources and wants to create new resources in YAML format - `combine` will be able to handle it properly. 97 | 98 | # Render from schema 99 | 100 | ```bash 101 | $ prmd render --template schemata.erb schema.json > schema.md 102 | ``` 103 | 104 | Typically you'll want to prepend header and overview information to 105 | your API documentation. You can do this with the `--prepend` flag: 106 | 107 | ```bash 108 | $ prmd doc --prepend overview.md schema.json > schema.md 109 | ``` 110 | 111 | You can also chain commands together as needed, e.g.: 112 | 113 | ```bash 114 | $ prmd combine --meta meta.json schemata/ | prmd verify | prmd doc --prepend overview.md > schema.md 115 | ``` 116 | 117 | See `prmd --help` for additional usage details. 118 | 119 | ## Documentation rendering settings 120 | 121 | Out of the box you can supply a settings file (in either `JSON` or `YAML`) that will tweak the layout of your documentation. 122 | 123 | ```bash 124 | $ prmd doc --settings config.json schema.json > schema.md 125 | ``` 126 | 127 | Available options (and their defaults) 128 | ```js 129 | { 130 | "doc": { 131 | "url_style": "default", // can also be "json" 132 | "disable_title_and_description": false, // remove the title and the description, useful when using your own custom header 133 | "toc": false // insert the table of content for json scheme documentation to the top of the file. (default disable) 134 | } 135 | } 136 | ``` 137 | 138 | ## Use as rake task 139 | 140 | In addition, prmd can be used via rake tasks 141 | 142 | ```ruby 143 | # Rakefile 144 | require 'prmd/rake_tasks/combine' 145 | require 'prmd/rake_tasks/verify' 146 | require 'prmd/rake_tasks/doc' 147 | 148 | namespace :schema do 149 | Prmd::RakeTasks::Combine.new do |t| 150 | t.options[:meta] = 'schema/meta.json' # use meta.yml if you prefer YAML format 151 | t.paths << 'schema/schemata/api' 152 | t.output_file = 'schema/api.json' 153 | end 154 | 155 | Prmd::RakeTasks::Verify.new do |t| 156 | t.files << 'schema/api.json' 157 | end 158 | 159 | Prmd::RakeTasks::Doc.new do |t| 160 | t.files = { 'schema/api.json' => 'schema/api.md' } 161 | end 162 | end 163 | 164 | task default: ['schema:combine', 'schema:verify', 'schema:doc'] 165 | ``` 166 | 167 | ## File Layout 168 | 169 | We suggest the following file layout for JSON schema related files: 170 | 171 | ``` 172 | /docs (top-level directory for project documentation) 173 | /schema (API schema documentation) 174 | /schemata 175 | /{resource.[json,yml]} (individual resource schema) 176 | /meta.[json,yml] (overall API metadata) 177 | /overview.md (preamble for generated API docs) 178 | /schema.json (complete generated JSON schema file) 179 | /schema.md (complete generated API documentation file) 180 | ``` 181 | 182 | where `[json,yml]` means that it could be either `json` or `yml`. 183 | 184 | ## Contributing 185 | 186 | 1. Fork it 187 | 2. Create your feature branch (`git checkout -b my-new-feature`) 188 | 3. Commit your changes (`git commit -am 'Add some feature'`) 189 | 4. Push to the branch (`git push origin my-new-feature`) 190 | 5. Create new Pull Request 191 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new do |t| 5 | t.pattern = "test/**/*_test.rb" 6 | end 7 | 8 | task default: :test 9 | -------------------------------------------------------------------------------- /bin/prmd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "optparse" 3 | require_relative "../lib/prmd" 4 | require_relative "../lib/prmd/cli" 5 | 6 | Prmd::CLI.run(ARGV.dup, bin: File.basename(__FILE__)) 7 | -------------------------------------------------------------------------------- /docs/schemata.md: -------------------------------------------------------------------------------- 1 | # schemata 2 | This document seeks to explain JSON schema in practice as well as our usage and associated implications. Everything described must be followed unless otherwise noted (or it is a bug). Unless otherwise noted (as in meta-data) keys should be alphabetized for ease of modification/review/updating. A great example in-the-wild is available for the Heroku Platform API, see [Heroku Devcenter](https://devcenter.heroku.com/articles/platform-api-reference#schema) for details. 3 | 4 | ## json-schema 5 | 6 | JSON Schema provides a way to describe the resources, attributes and links of an API using JSON. This document will contain many examples and explanation, but going to the source can also be useful. There are three relevant specs, which are additive. You can read through them, ideally in this order: 7 | 8 | 1. [JSON Schema Core](http://tools.ietf.org/html/draft-zyp-json-schema-04) - defines the basic foundation of JSON Schema - you probably will not need this often 9 | 2. [JSON Schema Validation](http://tools.ietf.org/html/draft-fge-json-schema-validation-00) - defines the validation keywords of JSON Schema - covers most attributes 10 | 3. [JSON Hyper-Schema](http://tools.ietf.org/html/draft-luff-json-hyper-schema-00) - defines the hyper-media keywords of JSON Schema - covers remaining links-specific attributes 11 | 12 | ## structure 13 | 14 | We have opted to split apart the schema into individual resource schema and a root schema which references all of them. These individual schema are named based on the singular form of the resource in question. 15 | 16 | ### meta-data 17 | 18 | Each schema MUST include some meta-data, which we cluster at the top of the file, including: 19 | 20 | * `description` - a description of the resource described by the schema 21 | * `id` - an id for this schema, it MUST be in the form `"schemata/#{lower_case_singular_resource}"` 22 | * `$schema` - defines what meta-schema is in use, it MUST be `http://json-schema.org/draft-04/hyper-schema` 23 | * `title` - title for this resource, it MUST be in the form `"#{title_case_API_name} - #{title_case_plural_resource}"` 24 | * `type` - the type(s) of this schema, it MUST be `["object"]` 25 | 26 | ### `definitions` 27 | 28 | We make heavy usage of the `definitions` attribute in each resource to provide a centralized collection of attributes related to each resource. By doing so we are able to refer to the same attribute in links, properties and even as foreign keys. 29 | 30 | The definitions object MUST include every attribute related directly to this resource, including: 31 | 32 | * all properties that are present in the serialization of the object 33 | * an `identity` property to provide an easy way to find what unique identifier(s) can be used with this object as well as what to use for foreign keys 34 | * all transient properties which may be passed into links related to the object, even if they are not serialized 35 | 36 | Each attribute MUST include the following properties: 37 | 38 | * `description` - a description of the attribute and how it relates to the resource 39 | * `example` - an example of the attributes value, useful for documentation and tests 40 | * `type` - an array of type(s) for this attribute, values MUST be one of `["array", "boolean", "integer", "number", "null", "object", "string"]` 41 | 42 | Each attribute MAY include the following properties: 43 | 44 | * `pattern` - a javascript regex encoded in a string that the valid values MUST match 45 | * `format` - format of the value. MUST be one of spec defined `["date", "date-time", "email", "hostname", "ipv4", "ipv6", "uri"]` or defined by us `["uuid"]` 46 | 47 | Examples: 48 | 49 | ```javascript 50 | { 51 | "definitions": { 52 | "id": { 53 | "description": "unique identifier of resource", 54 | "example": "01234567-89ab-cdef-0123-456789abcdef", 55 | "format": "uuid", 56 | "type": ["string"] 57 | }, 58 | "identity": { 59 | "anyOf": [ 60 | { "$ref": "/schemata/example#/definitions/id" } 61 | ] 62 | }, 63 | "url": { 64 | "description": "URL of resource", 65 | "example": "http://example.com", 66 | "format": "uri", 67 | "pattern": "^http://[a-z][a-z0-9-]{3,30}\\.com$", 68 | "type": ["null", "string"] 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | ### `links` 75 | 76 | Links define the actions available on a given resource. They are listed as an array, which should be alphabetized by title. 77 | 78 | The links array MUST include an object defining each action available. Each action MUST include the following attributes: 79 | 80 | * `description` - a description of the action to perform 81 | * `href` - the path associated with this action, use [URI templates](http://tools.ietf.org/html/rfc6570) as needed, CGI escaping any JSON pointer values used for identity 82 | * `method` - the http method to be used with this action 83 | * `rel` - describes relation of link to resource, SHOULD be one of `["create", "destroy", "self", "instances", "update"]` 84 | * `title` - title for the link 85 | 86 | Links that expect a json-encoded body as input MUST also include the following attributes: 87 | * `schema` - an object with a `properties` object that MUST include JSON pointers to the definitions for each associated attribute 88 | 89 | The `schema` object MAY also include a `required` array to define all attributes for this link, which can not be omitted. 90 | If this field is not present, all attributes in this link are considered as optional. 91 | 92 | Links that expect a custom http header MUST include the following attributes: 93 | * `http_header` - an object which has the key as the header name, and value as an example header value. 94 | 95 | ```javascript 96 | { 97 | "links": [ 98 | { 99 | "description": "Create a new resource.", 100 | "href": "/resources", 101 | "method": "POST", 102 | "rel": "create", 103 | "http_header": { "Custom-Header": "examplevalue" }, 104 | "schema": { 105 | "properties": { 106 | "owner": { "$ref": "/schemata/user#/definitions/identity" }, 107 | "url": { "$ref": "/schemata/resource/definitions/url" } 108 | }, 109 | "required": [ "owner", "url" ] 110 | }, 111 | "title": "Create" 112 | }, 113 | { 114 | "description": "Delete an existing resource.", 115 | "href": "/resources/{(%2Fschemata%2Fresources%23%2Fdefinitions%2Fidentity)}", 116 | "method": "DELETE", 117 | "rel": "destroy", 118 | "title": "Delete" 119 | }, 120 | { 121 | "description": "Info for existing resource.", 122 | "href": "/resources/{(%2Fschemata%2Fresources%23%2Fdefinitions%2Fidentity)}", 123 | "method": "GET", 124 | "rel": "self", 125 | "title": "Info" 126 | }, 127 | { 128 | "description": "List existing resources.", 129 | "href": "/resources", 130 | "method": "GET", 131 | "rel": "instances", 132 | "title": "List" 133 | }, 134 | { 135 | "description": "Update an existing resource.", 136 | "href": "/resources/{(%2Fschemata%2Fresource%23%2Fdefinitions%2Fidentity)}", 137 | "method": "PATCH", 138 | "rel": "update", 139 | "schema": { 140 | "properties": { 141 | "url": { "$ref": "/schemata/resource/definitions/url" } 142 | } 143 | }, 144 | "title": "Update" 145 | } 146 | ] 147 | } 148 | ``` 149 | 150 | Links MAY specify a different serialization than defined in [properties](#properties) via `targetSchema`. 151 | 152 | ### `properties` 153 | 154 | Properties defines the attributes that exist in the serialization of the object. 155 | 156 | The properties object MUST contain all the serialized attributes for the object. Each attribute MUST provide a [JSON pointer](http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-07) to the attribute in appropriate `definitions`, we have opted to always use absolute pointers for consistency. Properties MUST also add any data which overrides the values in definitions and SHOULD add any additional, relevant data. 157 | 158 | ```javascript 159 | { 160 | "properties": { 161 | "id": { "$ref": "/schemata/resource#/definitions/id" }, 162 | "owner": { 163 | "description": "unique identifier of the user who owns this resource", 164 | "properties": { 165 | "id": { "$ref": "/schemata/user#/definitions/id" } 166 | }, 167 | "type": ["object"] 168 | }, 169 | "url": { "$ref": "/schemata/resource#/definitions/url" } 170 | } 171 | } 172 | ``` 173 | 174 | Note: this assumes that schema/user will also be available and will have id defined in the definitions. If/when you need to refer to a foreign key, you MUST add a new schema and/or add the appropriate attribute to the foreign resource definitions unless it already exists. 175 | -------------------------------------------------------------------------------- /lib/prmd.rb: -------------------------------------------------------------------------------- 1 | require_relative "prmd/version" 2 | require_relative "prmd/load_schema_file" 3 | require_relative "prmd/commands" 4 | require_relative "prmd/schema" 5 | require_relative "prmd/link" 6 | require_relative "prmd/utils" 7 | require_relative "prmd/template" 8 | require_relative "prmd/url_generator" 9 | require_relative "prmd/hash_helpers" 10 | -------------------------------------------------------------------------------- /lib/prmd/cli.rb: -------------------------------------------------------------------------------- 1 | require_relative "core_ext/optparse" 2 | require_relative "cli/combine" 3 | require_relative "cli/doc" 4 | require_relative "cli/generate" 5 | require_relative "cli/render" 6 | require_relative "cli/stub" 7 | require_relative "cli/verify" 8 | 9 | # :nodoc: 10 | module Prmd 11 | # Main CLI module 12 | module CLI 13 | # @param [Hash] props 14 | # @return [Hash] all dem parsers 15 | def self.make_command_parsers(props = {}) 16 | { 17 | combine: CLI::Combine.make_parser(props), 18 | doc: CLI::Doc.make_parser(props), 19 | init: CLI::Generate.make_parser(props), 20 | render: CLI::Render.make_parser(props), 21 | stub: CLI::Stub.make_parser(props), 22 | verify: CLI::Verify.make_parser(props), 23 | } 24 | end 25 | 26 | # List of all available commands 27 | # 28 | # @return [Array] available commands 29 | def self.commands 30 | @commands ||= make_command_parsers.keys 31 | end 32 | 33 | # Creates the CLI main parser 34 | # 35 | # @param [Hash] options 36 | # @param [Hash] props 37 | def self.make_parser(options, props = {}) 38 | binname = props.fetch(:bin, "prmd") 39 | 40 | # This is only used to attain the help commands 41 | commands = make_command_parsers(props) 42 | help_text = commands.values.map do |command| 43 | " #{command.banner}" 44 | end.join("\n") 45 | 46 | OptionParser.new do |opts| 47 | opts.banner = "Usage: #{binname} [options] [command [options]]" 48 | opts.separator "\nAvailable options:" 49 | opts.on("--version", "Return version") do 50 | puts "prmd #{Prmd::VERSION}" 51 | exit(0) 52 | end 53 | opts.on("--noop", "Commands will not execute") do |v| 54 | options[:noop] = v 55 | end 56 | opts.separator "\nAvailable commands:" 57 | opts.separator help_text 58 | end 59 | end 60 | 61 | # Parse top level CLI options from argv 62 | # 63 | # @param [Array] argv 64 | # @param [Hash] opts 65 | # @return [Hash] parsed options 66 | def self.parse_options(argv, opts = {}) 67 | options = {} 68 | parser = make_parser(options, opts) 69 | abort parser if argv.empty? 70 | com_argv = parser.order(argv) 71 | abort parser if com_argv.empty? 72 | command = com_argv.shift.to_sym 73 | abort parser unless commands.include?(command) 74 | options[:argv] = com_argv 75 | options[:command] = command 76 | options 77 | end 78 | 79 | # Execute the Prmd CLI, or its subcommands 80 | # 81 | # @param [Array] uargv 82 | # @param [Hash] opts 83 | # @return [void] 84 | def self.run(uargv, opts = {}) 85 | options = parse_options(uargv, opts) 86 | argv = options.delete(:argv) 87 | command = options.delete(:command) 88 | 89 | case command 90 | when :combine 91 | CLI::Combine.run(argv, options) 92 | when :doc 93 | CLI::Doc.run(argv, options) 94 | when :init 95 | CLI::Generate.run(argv, options) 96 | when :render 97 | CLI::Render.run(argv, options) 98 | when :stub 99 | CLI::Stub.run(argv, options) 100 | when :verify 101 | CLI::Verify.run(argv, options) 102 | end 103 | end 104 | 105 | class << self 106 | private :make_command_parsers 107 | private :commands 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/prmd/cli/base.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require_relative "../core_ext/optparse" 3 | require_relative "../load_schema_file" 4 | 5 | module Prmd 6 | module CLI 7 | # Base module for CLI commands. 8 | # @api 9 | module Base 10 | # Create a parser specific for this command. 11 | # The parsers produced by this method should yield their options. 12 | # 13 | # @example Overwriting 14 | # def make_parser(options = {}) 15 | # OptionParser.new do |opts| 16 | # opts.on("-v", "--verbose", "set verbose debugging") do |v| 17 | # yield :verbose, v 18 | # end 19 | # end 20 | # end 21 | # 22 | # @param [Hash] options 23 | # @return [OptionParser] newly created parser 24 | # @abstract 25 | def make_parser(options = {}) 26 | end 27 | 28 | # Runs the provided parser with the provided argv. 29 | # This method can be overwritten to use a different parser method. 30 | # 31 | # @param [OptionParser] parser 32 | # @param [Array] argv 33 | # @return [Array] remaining arguments 34 | # @private 35 | def execute_parser(argv) 36 | @parser.parse(argv) 37 | end 38 | 39 | # Set the given key and value in the given options Hash. 40 | # This method can ben overwritten to support special keys or values 41 | # 42 | # @example Handling special keys 43 | # def self.set_option(options, key, value) 44 | # if key == :settings 45 | # options.replace(value.merge(options)) 46 | # else 47 | # super 48 | # end 49 | # end 50 | # 51 | # @param [Hash] options 52 | # @param [Symbol] key 53 | # @param [Object] value 54 | # @return [void] 55 | def set_option(options, key, value) 56 | options[key] = value 57 | end 58 | 59 | # Parse the given argv and produce a options Hash specific to the command. 60 | # The returned options Hash will include an :argv key which contains 61 | # the remaining args from the parse operation. 62 | # 63 | # @param [Array] argv 64 | # @param [Hash] options 65 | # @return [Hash] parsed options 66 | def parse_options(argv, options = {}) 67 | opts = {} 68 | @parser = make_parser(options) do |key, value| 69 | set_option(opts, key, value) 70 | end 71 | argv = execute_parser(argv) 72 | opts[:argv] = argv 73 | opts 74 | end 75 | 76 | # Helper method for writing command results to a file or STD* IO. 77 | # 78 | # @param [String] data to be written 79 | # @param [Hash] options 80 | # @return [void] 81 | def write_result(data, options = {}) 82 | output_file = options[:output_file] 83 | if output_file 84 | File.write(output_file, data) 85 | else 86 | $stdout.puts data 87 | end 88 | end 89 | 90 | # Helper method for reading schema data from a file or STD* IO. 91 | # 92 | # @param [String] filename file to read 93 | # @return [Array[Symbol, String]] source, data 94 | def try_read(filename = nil) 95 | if filename && !filename.empty? 96 | [:file, Prmd.load_schema_file(filename)] 97 | elsif !$stdin.tty? 98 | [:io, JSON.load($stdin.read)] 99 | else 100 | abort "Nothing to read" 101 | end 102 | end 103 | 104 | # Method called to actually execute the command provided with an 105 | # options Hash from the commands #parse_options. 106 | # 107 | # @param [Hash] options 108 | # @return [void] 109 | # @abstract 110 | def execute(options = {}) 111 | end 112 | 113 | # Method called when the command is ran with the :noop option enabled. 114 | # As the option implies, this should do absolutely nothing. 115 | # 116 | # @param [Hash] options 117 | # @return [void] 118 | def noop_execute(options = {}) 119 | warn options 120 | end 121 | 122 | # Run this command given a argv and optional options Hash. 123 | # If all you have is the options from the #parse_options method, use 124 | # #execute instead. 125 | # 126 | # @see #execute 127 | # @see #parse_options 128 | # 129 | # @param [Array] argv 130 | # @param [Hash] options 131 | # @return [void] 132 | def run(argv, options = {}) 133 | options = options.merge(parse_options(argv, options)) 134 | if options[:noop] 135 | noop_execute(options) 136 | else 137 | execute(options) 138 | end 139 | end 140 | 141 | private :execute_parser 142 | private :set_option 143 | private :write_result 144 | private :try_read 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /lib/prmd/cli/combine.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require_relative "../commands/combine" 3 | 4 | module Prmd 5 | module CLI 6 | # 'combine' command module. 7 | module Combine 8 | extend CLI::Base 9 | 10 | # Returns a OptionParser for parsing 'combine' command options. 11 | # 12 | # @param (see Prmd::CLI::Base#make_parser) 13 | # @return (see Prmd::CLI::Base#make_parser) 14 | def self.make_parser(options = {}) 15 | binname = options.fetch(:bin, "prmd") 16 | 17 | OptionParser.new do |opts| 18 | opts.banner = "#{binname} combine [options] " 19 | opts.on("-m", "--meta FILENAME", String, "Set defaults for schemata") do |m| 20 | yield :meta, m 21 | end 22 | opts.on("-o", "--output-file FILENAME", String, "File to write result to") do |n| 23 | yield :output_file, n 24 | end 25 | opts.on("-t", "--type-as-string", "Allow type as string") do |t| 26 | options[:type_as_string] = t 27 | end 28 | end 29 | end 30 | 31 | # Executes the 'combine' command. 32 | # 33 | # @example Usage 34 | # Prmd::CLI::Combine.execute(argv: ['schema/schemata/api'], 35 | # meta: 'schema/meta.json', 36 | # output_file: 'schema/api.json', 37 | # type-as-string) 38 | # 39 | # @param (see Prmd::CLI::Base#execute) 40 | # @return (see Prmd::CLI::Base#execute) 41 | def self.execute(options = {}) 42 | write_result Prmd.combine(options[:argv], options).to_s, options 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/prmd/cli/doc.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require_relative "../commands/render" 3 | require_relative "../hash_helpers" 4 | 5 | module Prmd 6 | module CLI 7 | # 'doc' command module. 8 | module Doc 9 | extend CLI::Base 10 | 11 | # Returns a OptionParser for parsing 'doc' command options. 12 | # 13 | # @param (see Prmd::CLI::Base#make_parser) 14 | # @return (see Prmd::CLI::Base#make_parser) 15 | def self.make_parser(options = {}) 16 | binname = options.fetch(:bin, "prmd") 17 | 18 | OptionParser.new do |opts| 19 | opts.banner = "#{binname} doc [options] " 20 | opts.on("-s", "--settings FILENAME", String, "Config file to use") do |s| 21 | settings = Prmd.load_schema_file(s) || {} 22 | options = HashHelpers.deep_symbolize_keys(settings) 23 | yield :settings, options 24 | end 25 | opts.on("-c", "--content-type application/json", String, "Content-Type header") do |c| 26 | yield :content_type, c 27 | end 28 | opts.on("-o", "--output-file FILENAME", String, "File to write result to") do |n| 29 | yield :output_file, n 30 | end 31 | opts.on("-p", "--prepend header,overview", Array, "Prepend files to output") do |p| 32 | yield :prepend, p 33 | end 34 | end 35 | end 36 | 37 | # Overwritten to support :settings merging. 38 | # 39 | # @see Prmd::CLI::Base#set_option 40 | # 41 | # @param (see Prmd::CLI::Base#set_option) 42 | # @return (see Prmd::CLI::Base#set_option) 43 | def self.set_option(options, key, value) 44 | if key == :settings 45 | options.replace(value.merge(options)) 46 | else 47 | super 48 | end 49 | end 50 | 51 | # Executes the 'doc' command. 52 | # 53 | # @example Usage 54 | # Prmd::CLI::Doc.execute(argv: ['schema/api.json'], 55 | # output_file: 'schema/api.md') 56 | # 57 | # @param (see Prmd::CLI::Base#execute) 58 | # @return (see Prmd::CLI::Base#execute) 59 | def self.execute(options = {}) 60 | filename = options.fetch(:argv).first 61 | template = File.expand_path("templates", File.dirname(__FILE__)) 62 | _, data = try_read(filename) 63 | schema = Prmd::Schema.new(data) 64 | opts = options.merge(template: template) 65 | write_result Prmd.render(schema, opts), options 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/prmd/cli/generate.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require_relative "../commands/init" 3 | require_relative "../utils" 4 | 5 | module Prmd 6 | module CLI 7 | # 'init' command module. 8 | # Though this is called, Generate, it is used by the init method for 9 | # creating new Schema files 10 | module Generate 11 | extend CLI::Base 12 | 13 | # Returns a OptionParser for parsing 'init' command options. 14 | # 15 | # @param (see Prmd::CLI::Base#make_parser) 16 | # @return (see Prmd::CLI::Base#make_parser) 17 | def self.make_parser(options = {}) 18 | binname = options.fetch(:bin, "prmd") 19 | 20 | OptionParser.new do |opts| 21 | opts.banner = "#{binname} init [options] " 22 | opts.on("-t", "--template templates", String, "Use alternate template") do |t| 23 | yield :template, t 24 | end 25 | opts.on("-y", "--yaml", "Generate YAML") do |y| 26 | yield :yaml, y 27 | end 28 | opts.on("-o", "--output-file FILENAME", String, "File to write result to") do |n| 29 | yield :output_file, n 30 | end 31 | end 32 | end 33 | 34 | # Executes the 'init' command. 35 | # 36 | # @example Usage 37 | # Prmd::CLI::Generate.execute(argv: ['bread'], 38 | # output_file: 'schema/schemata/bread.json') 39 | # 40 | # @param (see Prmd::CLI::Base#execute) 41 | # @return (see Prmd::CLI::Base#execute) 42 | def self.execute(options = {}) 43 | name = options.fetch(:argv).first 44 | if Prmd::Utils.blank?(name) 45 | abort @parser 46 | else 47 | write_result Prmd.init(name, options), options 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/prmd/cli/render.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require_relative "../commands/render" 3 | 4 | module Prmd 5 | module CLI 6 | # 'render' command module. 7 | module Render 8 | extend CLI::Base 9 | 10 | # Returns a OptionParser for parsing 'render' command options. 11 | # 12 | # @param (see Prmd::CLI::Base#make_parser) 13 | # @return (see Prmd::CLI::Base#make_parser) 14 | def self.make_parser(options = {}) 15 | binname = options.fetch(:bin, "prmd") 16 | 17 | OptionParser.new do |opts| 18 | opts.banner = "#{binname} render [options] " 19 | opts.on("-c", "--content-type application/json", String, "Content-Type header") do |c| 20 | yield :content_type, c 21 | end 22 | opts.on("-o", "--output-file FILENAME", String, "File to write result to") do |n| 23 | yield :output_file, n 24 | end 25 | opts.on("-p", "--prepend header,overview", Array, "Prepend files to output") do |p| 26 | yield :prepend, p 27 | end 28 | opts.on("-t", "--template templates", String, "Use alternate template") do |t| 29 | yield :template, t 30 | end 31 | end 32 | end 33 | 34 | # Executes the 'render' command. 35 | # 36 | # @example Usage 37 | # Prmd::CLI::Render.execute(argv: ['schema/api.json'], 38 | # template: 'my_template.md.erb', 39 | # output_file: 'schema/api.md') 40 | # 41 | # @param (see Prmd::CLI::Base#execute) 42 | # @return (see Prmd::CLI::Base#execute) 43 | def self.execute(options = {}) 44 | filename = options.fetch(:argv).first 45 | _, data = try_read(filename) 46 | schema = Prmd::Schema.new(data) 47 | write_result Prmd.render(schema, options), options 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/prmd/cli/stub.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | module Prmd 4 | module CLI 5 | # 'stub' command module' 6 | module Stub 7 | extend CLI::Base 8 | 9 | # Returns a OptionParser for parsing 'stub' command options. 10 | # 11 | # @param (see Prmd::CLI::Base#make_parser) 12 | # @return (see Prmd::CLI::Base#make_parser) 13 | def self.make_parser(options = {}) 14 | binname = options.fetch(:bin, "prmd") 15 | 16 | OptionParser.new do |opts| 17 | opts.banner = "#{binname} stub [options] " 18 | end 19 | end 20 | 21 | # Executes the 'stub' command. 22 | # 23 | # @example Usage 24 | # Prmd::CLI::Stub.execute(argv: ['schema/api.json']) 25 | # 26 | # @param (see Prmd::CLI::Base#execute) 27 | # @return (see Prmd::CLI::Base#execute) 28 | def self.execute(options = {}) 29 | require "committee" 30 | 31 | filename = options.fetch(:argv).first 32 | _, schema = try_read(filename) 33 | 34 | app = Rack::Builder.new { 35 | use Committee::Middleware::RequestValidation, schema: schema 36 | use Committee::Middleware::ResponseValidation, schema: schema 37 | use Committee::Middleware::Stub, schema: schema 38 | run lambda { |_| [404, {}, ["Not found"]] } 39 | } 40 | 41 | Rack::Server.start(app: app) 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/prmd/cli/verify.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require_relative "../commands/verify" 3 | 4 | module Prmd 5 | module CLI 6 | # 'verify' command module. 7 | module Verify 8 | extend CLI::Base 9 | 10 | # Returns a OptionParser for parsing 'verify' command options. 11 | # 12 | # @param (see Prmd::CLI::Base#make_parser) 13 | # @return (see Prmd::CLI::Base#make_parser) 14 | def self.make_parser(options = {}) 15 | binname = options.fetch(:bin, "prmd") 16 | 17 | OptionParser.new do |opts| 18 | opts.banner = "#{binname} verify [options] " 19 | opts.on("-y", "--yaml", "Generate YAML") do |y| 20 | yield :yaml, y 21 | end 22 | opts.on("-o", "--output-file FILENAME", String, "File to write result to") do |n| 23 | yield :output_file, n 24 | end 25 | opts.on("-s", "--custom-schema FILENAME", String, "Path to custom schema") do |n| 26 | yield :custom_schema, n 27 | end 28 | end 29 | end 30 | 31 | # Executes the 'verify' command. 32 | # 33 | # @example Usage 34 | # Prmd::CLI::Verify.execute(argv: ['schema/api.json']) 35 | # 36 | # @param (see Prmd::CLI::Base#execute) 37 | # @return (see Prmd::CLI::Base#execute) 38 | def self.execute(options = {}) 39 | filename = options.fetch(:argv).first 40 | _, data = try_read(filename) 41 | custom_schema = options[:custom_schema] 42 | errors = Prmd.verify(data, custom_schema: custom_schema) 43 | unless errors.empty? 44 | errors.map! { |error| "#{filename}: #{error}" } if filename 45 | errors.each { |error| warn(error) } 46 | exit(1) 47 | end 48 | result = options[:yaml] ? data.to_yaml : JSON.pretty_generate(data) 49 | write_result result, options 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/prmd/commands.rb: -------------------------------------------------------------------------------- 1 | require_relative "commands/combine" 2 | require_relative "commands/init" 3 | require_relative "commands/render" 4 | require_relative "commands/verify" 5 | -------------------------------------------------------------------------------- /lib/prmd/commands/combine.rb: -------------------------------------------------------------------------------- 1 | require_relative "../load_schema_file" 2 | require_relative "../core/schema_hash" 3 | require_relative "../core/combiner" 4 | 5 | # :nodoc: 6 | module Prmd 7 | # Schema combine 8 | module Combine 9 | # @api private 10 | # @param [#size] given 11 | # @param [#size] expected 12 | # @return [void] 13 | def self.handle_faulty_load(given, expected) 14 | unless given.size == expected.size 15 | abort "Somes files have failed to parse. " \ 16 | "If you wish to continue without them," \ 17 | "please enable faulty_load using --faulty-load" 18 | end 19 | end 20 | 21 | # @api private 22 | # @param [Array] paths 23 | # @param [Hash] options 24 | # @return [Array] list of filenames from paths 25 | def self.crawl_map(paths, options = {}) 26 | files = [*paths].map do |path| 27 | if File.directory?(path) 28 | Dir.glob(File.join(path, "**", "*.{json,yml,yaml}")) 29 | else 30 | path 31 | end 32 | end 33 | files.flatten! 34 | files.delete(options[:meta]) 35 | files 36 | end 37 | 38 | # @api private 39 | # @param [String] filename 40 | # @return [SchemaHash] 41 | def self.load_schema_hash(filename) 42 | data = Prmd.load_schema_file(filename) 43 | SchemaHash.new(data, filename: filename) 44 | end 45 | 46 | # @api private 47 | # @param [Array] files 48 | # @param [Hash] options 49 | # @return [Array] schema hashes 50 | def self.load_files(files, options = {}) 51 | files.each_with_object([]) do |filename, result| 52 | result << load_schema_hash(filename) 53 | rescue JSON::ParserError, Psych::SyntaxError => ex 54 | warn "unable to parse #{filename} (#{ex.inspect})" 55 | end 56 | end 57 | 58 | # @api private 59 | # @param [Array] paths 60 | # @param [Hash] options 61 | # @return (see .load_files) 62 | def self.load_schemas(paths, options = {}) 63 | files = crawl_map(paths, options) 64 | # sort for stable loading across platforms 65 | schemata = load_files(files.sort, options) 66 | handle_faulty_load(schemata, files) unless options[:faulty_load] 67 | schemata 68 | end 69 | 70 | # Escape '#' and '/' in 'href' keys. They need to be escaped in JSON schema, 71 | # but to make it easier to write JSON schema with Prmd, those two characters 72 | # are escaped automatically when they appear between '{()}'. 73 | # See https://github.com/interagent/prmd/issues/106. 74 | # 75 | # @api private 76 | # @param [Array] schema hashes 77 | # @return [Array] schema hashes 78 | def self.escape_hrefs(data) 79 | if data.is_a? Array 80 | data.map! { |x| 81 | escape_hrefs(x) 82 | } 83 | elsif data.is_a?(Hash) || data.is_a?(Prmd::SchemaHash) 84 | data.each { |k, v| 85 | if k == "href" 86 | if v.is_a? String 87 | v = v.gsub(/\{\(.*?\)\}/) { |x| 88 | x.gsub("#", "%23").gsub("/", "%2F") 89 | } 90 | end 91 | else 92 | v = escape_hrefs(v) 93 | end 94 | data[k] = v 95 | } 96 | end 97 | data 98 | end 99 | 100 | # Merges all found schema files in the given paths into a single Schema 101 | # 102 | # @param [Array] paths 103 | # @param [Hash] options 104 | # @return (see Prmd::Combiner#combine) 105 | def self.combine(paths, options = {}) 106 | schemata = escape_hrefs(load_schemas(paths)) 107 | base = Prmd::Template.load_json("combine_head.json") 108 | schema = base["$schema"] 109 | meta = {} 110 | filename = options[:meta] 111 | meta = Prmd.load_schema_file(filename) if filename 112 | if meta.nil? || meta.empty? 113 | if filename 114 | warn "Meta file (#{filename}) is empty, please fill it next time." 115 | else 116 | warn "Meta is empty, please fill it next time." 117 | end 118 | meta ||= {} 119 | end 120 | combiner = Prmd::Combiner.new(meta: meta, base: base, schema: schema, options: options) 121 | combiner.combine(*schemata) 122 | end 123 | 124 | class << self 125 | private :handle_faulty_load 126 | private :crawl_map 127 | private :load_schema_hash 128 | private :load_files 129 | private :load_schemas 130 | private :escape_hrefs 131 | end 132 | end 133 | 134 | # (see Prmd::Combine.combine) 135 | def self.combine(paths, options = {}) 136 | Combine.combine(paths, { faulty_load: false }.merge(options)) 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /lib/prmd/commands/init.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require_relative "../template" 3 | require_relative "../core/generator" 4 | 5 | # :nodoc: 6 | module Prmd 7 | # Schema generation 8 | module Generate 9 | # Creates a default Prmd::Generator using default templates 10 | # 11 | # @return [Prmd::Generator] 12 | def self.make_generator(options) 13 | base = Prmd::Template.load_json("init_default.json") 14 | template_name = options.fetch(:template) do 15 | abort "render: Template was not provided" 16 | end 17 | if template_name && !template_name.empty? 18 | template_dir = File.expand_path(template_name) 19 | # to keep backward compatibility 20 | template_dir = File.dirname(template_name) unless File.directory?(template_dir) 21 | template_name = File.basename(template_name) 22 | else 23 | template_name = "init_resource.json.erb" 24 | template_dir = "" 25 | end 26 | template = Prmd::Template.load_template(template_name, template_dir) 27 | Prmd::Generator.new(base: base, template: template) 28 | end 29 | end 30 | 31 | # Generate a schema template 32 | # 33 | # @param [String] resource 34 | # @param [Hash] options 35 | # @return [String] schema template in YAML (yaml option was enabled) else JSON 36 | def self.init(resource, options = {}) 37 | gen = Generate.make_generator(template: options[:template]) 38 | 39 | generator_options = { resource: nil, parent: nil } 40 | if resource 41 | parent = nil 42 | parent, resource = resource.split("/") if resource.include?("/") 43 | generator_options[:parent] = parent 44 | generator_options[:resource] = resource 45 | end 46 | 47 | schema = gen.generate(generator_options) 48 | 49 | if options[:yaml] 50 | schema.to_yaml 51 | else 52 | schema.to_json 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/prmd/commands/render.rb: -------------------------------------------------------------------------------- 1 | require_relative "../core/renderer" 2 | 3 | # :nodoc: 4 | module Prmd 5 | # Render helper module 6 | module Render 7 | # Retrieve the schema template 8 | # 9 | # @param [Hash] options 10 | # @return (see Prmd::Template.load_template) 11 | def self.get_template(options) 12 | template = options.fetch(:template) do 13 | abort "render: Template was not provided" 14 | end 15 | template_dir = File.expand_path(template) 16 | # to keep backward compatibility 17 | template_dir = File.dirname(template) unless File.directory?(template_dir) 18 | Prmd::Template.load_template("schema.erb", template_dir) 19 | end 20 | end 21 | 22 | # Render provided schema to Markdown 23 | # 24 | # @param [Prmd::Schema] schema 25 | # @return [String] rendered schema in Markdown 26 | def self.render(schema, options = {}) 27 | renderer = Prmd::Renderer.new(template: Render.get_template(options)) 28 | doc = "" 29 | if options[:prepend] 30 | doc << 31 | options[:prepend].map { |path| File.read(path) }.join("\n") << 32 | "\n" 33 | end 34 | doc << renderer.render(schema, options) 35 | doc 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/prmd/commands/verify.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "json_schema" 3 | 4 | # :nodoc: 5 | module Prmd 6 | # Schema Verification 7 | module Verification 8 | # These schemas are listed manually and in order because they reference each 9 | # other. 10 | SCHEMAS = [ 11 | "schema.json", 12 | "hyper-schema.json", 13 | "interagent-hyper-schema.json", 14 | ] 15 | 16 | # @return [JsonSchema::DocumentStore] 17 | def self.init_document_store 18 | store = JsonSchema::DocumentStore.new 19 | SCHEMAS.each do |file| 20 | file = File.expand_path("../../../../schemas/#{file}", __FILE__) 21 | add_schema(store, file) 22 | end 23 | add_schema(store, @custom_schema) unless @custom_schema.nil? 24 | store 25 | end 26 | 27 | def self.add_schema(store, file) 28 | data = JSON.parse(File.read(file)) 29 | schema = JsonSchema::Parser.new.parse!(data) 30 | schema.expand_references!(store: store) 31 | store.add_schema(schema) 32 | end 33 | 34 | # @return [JsonSchema::DocumentStore] 35 | def self.document_store 36 | @document_store ||= init_document_store 37 | end 38 | 39 | # @param [Hash] schema_data 40 | # @return [Array] errors from failed verfication 41 | def self.verify_parsable(schema_data) 42 | # for good measure, make sure that the schema parses and that its 43 | # references can be expanded 44 | schema, errors = JsonSchema.parse!(schema_data) 45 | return JsonSchema::SchemaError.aggregate(errors) unless schema 46 | 47 | valid, errors = schema.expand_references(store: document_store) 48 | return JsonSchema::SchemaError.aggregate(errors) unless valid 49 | 50 | [] 51 | end 52 | 53 | # @param [Hash] schema_data 54 | # @return [Array] errors from failed verfication 55 | def self.verify_meta_schema(meta_schema, schema_data) 56 | valid, errors = meta_schema.validate(schema_data) 57 | return JsonSchema::SchemaError.aggregate(errors) unless valid 58 | 59 | [] 60 | end 61 | 62 | # @param [Hash] schema_data 63 | # @return [Array] errors from failed verfication 64 | def self.verify_schema(schema_data) 65 | schema_uri = schema_data["$schema"] 66 | return ["Missing $schema key."] unless schema_uri 67 | 68 | meta_schema = document_store.lookup_schema(schema_uri) 69 | return ["Unknown $schema: #{schema_uri}."] unless meta_schema 70 | 71 | verify_meta_schema(meta_schema, schema_data) 72 | end 73 | 74 | # Verfies that a given schema is valid 75 | # 76 | # @param [Hash] schema_data 77 | # @return [Array] errors from failed verification 78 | def self.verify(schema_data, custom_schema: nil) 79 | @custom_schema = custom_schema 80 | a = verify_schema(schema_data) 81 | return a unless a.empty? 82 | b = verify_parsable(schema_data) 83 | return b unless b.empty? 84 | [] 85 | end 86 | 87 | class << self 88 | private :init_document_store 89 | private :document_store 90 | private :verify_parsable 91 | private :verify_schema 92 | private :verify_meta_schema 93 | end 94 | end 95 | 96 | # (see Prmd::Verification.verify) 97 | def self.verify(schema_data, custom_schema: nil) 98 | Verification.verify(schema_data, custom_schema: custom_schema) 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/prmd/core/combiner.rb: -------------------------------------------------------------------------------- 1 | require_relative "../schema" 2 | require_relative "schema_hash" 3 | require_relative "reference_localizer" 4 | 5 | # :nodoc: 6 | module Prmd 7 | # Schema combiner 8 | class Combiner 9 | # 10 | # @param [Hash] properties 11 | def initialize(properties = {}) 12 | @properties = properties 13 | @schema = properties.fetch(:schema) 14 | @base = properties.fetch(:base, {}) 15 | @meta = properties.fetch(:meta, {}) 16 | @options = properties.fetch(:options, {}) 17 | end 18 | 19 | # @param [Object] datum 20 | # @return [Object] 21 | def reference_localizer(datum) 22 | ReferenceLocalizer.localize(datum) 23 | end 24 | 25 | # 26 | # @param [Prmd::SchemaHash] schemata 27 | # @return [Prmd::Schema] 28 | def combine(*schemata) 29 | # tracks which entities where defined in which file 30 | schemata_map = {} 31 | 32 | data = {} 33 | data.merge!(@base) 34 | data.merge!(@meta) 35 | 36 | schemata.each do |schema| 37 | id = schema.fetch("id") 38 | id_ary = id.split("/").last 39 | 40 | if s = schemata_map[id] 41 | warn "`#{id}` (from #{schema.filename}) was already defined " \ 42 | "in `#{s.filename}` and will overwrite the first " \ 43 | "definition" 44 | end 45 | # avoinding damaging the original schema 46 | embedded_schema = schema.dup 47 | # schemas are now in a single scope by combine 48 | embedded_schema.delete("id") 49 | schemata_map[id] = embedded_schema 50 | 51 | data["definitions"][id_ary] = embedded_schema.to_h 52 | data["properties"][id_ary] = { "$ref" => "#/definitions/#{id_ary}" } 53 | 54 | reference_localizer(data["definitions"][id_ary]) 55 | end 56 | 57 | Prmd::Schema.new(data, @options) 58 | end 59 | 60 | private :reference_localizer 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/prmd/core/generator.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require_relative "../schema" 3 | 4 | # :nodoc: 5 | module Prmd 6 | # Schema generator 7 | class Generator 8 | # 9 | # @param [Hash] properties 10 | def initialize(properties = {}) 11 | @properties = properties 12 | @base = properties.fetch(:base, {}) 13 | @template = properties.fetch(:template) 14 | end 15 | 16 | # 17 | # @param [Hash] options 18 | def generate(options = {}) 19 | res = @template.result(options) 20 | resource_schema = JSON.parse(res) 21 | schema = Prmd::Schema.new 22 | schema.merge!(@base) 23 | schema.merge!(resource_schema) 24 | schema 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/prmd/core/reference_localizer.rb: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | module Prmd 3 | # @api private 4 | # Schema references localizer 5 | class ReferenceLocalizer 6 | attr_reader :object 7 | 8 | # @param [Object] object 9 | def initialize(object) 10 | @object = object 11 | end 12 | 13 | # @param [Object] object 14 | # @return [ReferenceLocalizer] 15 | def self.build(object) 16 | case object 17 | when Array 18 | ForArray 19 | when Hash 20 | ForHash 21 | else 22 | self 23 | end.new(object) 24 | end 25 | 26 | # @param [Object] object 27 | # @return [Object] 28 | def self.localize(object) 29 | build(object).localize 30 | end 31 | 32 | # @return [Object] 33 | def localize 34 | object 35 | end 36 | 37 | private :object 38 | 39 | # @api private 40 | # Schema references localizer for arrays 41 | class ForArray < self 42 | alias_method :array, :object 43 | 44 | # @return [Array] 45 | def localize 46 | array.map { |element| ReferenceLocalizer.localize(element) } 47 | end 48 | end 49 | 50 | # @api private 51 | # Schema references localizer for hashes 52 | class ForHash < self 53 | alias_method :hash, :object 54 | 55 | # @return [Hash] 56 | def localize 57 | localize_ref 58 | localize_href 59 | localize_values 60 | end 61 | 62 | def localize_ref 63 | return unless hash.key?("$ref") 64 | hash["$ref"] = "#/definitions" + local_reference 65 | end 66 | 67 | def localize_href 68 | return unless hash.key?("href") && hash["href"].is_a?(String) 69 | hash["href"] = hash["href"].gsub("%23", "") 70 | .gsub(/%2Fschemata(%2F[^%]*%2F)/, 71 | '%23%2Fdefinitions\1',) 72 | end 73 | 74 | # @return [Hash] 75 | def localize_values 76 | hash.each_with_object({}) { |(k, v), r| r[k] = ReferenceLocalizer.localize(v) } 77 | end 78 | 79 | # @return [String] 80 | def local_reference 81 | ref = hash["$ref"] 82 | # clean out leading #/definitions to not create a duplicate one 83 | ref = ref.gsub(/^#\/definitions\//, "#/") while ref.match(/^#\/definitions\//) 84 | ref.gsub("#", "").gsub("/schemata", "") 85 | end 86 | 87 | private :localize_ref 88 | private :localize_href 89 | private :localize_values 90 | private :local_reference 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/prmd/core/renderer.rb: -------------------------------------------------------------------------------- 1 | require_relative "../template" 2 | 3 | # :nodoc: 4 | module Prmd 5 | # Schema Generator 6 | class Renderer 7 | # 8 | # @param [Hash] properties 9 | def initialize(properties = {}) 10 | @properties = properties 11 | @template = @properties.fetch(:template) 12 | end 13 | 14 | # 15 | # @return [Hash] 16 | def default_options 17 | { 18 | http_header: {}, 19 | content_type: "application/json", 20 | doc: {}, 21 | prepend: nil, 22 | } 23 | end 24 | 25 | # 26 | # @param [Hash] options 27 | # @return [void] 28 | def append_default_options(options) 29 | options[:doc] = { 30 | url_style: "default", 31 | disable_title_and_description: false, 32 | toc: false, 33 | }.merge(options[:doc]) 34 | end 35 | 36 | # 37 | # @param [Hash] options 38 | # @return [Hash] 39 | def setup_options(options) 40 | opts = default_options 41 | opts.merge!(options) 42 | append_default_options(opts) 43 | opts 44 | end 45 | 46 | # 47 | # @param [Prmd::Schema] schema 48 | # @param [Hash] options 49 | def render(schema, options = {}) 50 | @template.result(schema: schema, options: setup_options(options)) 51 | end 52 | 53 | private :default_options 54 | private :append_default_options 55 | private :setup_options 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/prmd/core/schema_hash.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | 3 | # :nodoc: 4 | module Prmd 5 | # Specialized Hash for handling loaded Schema data 6 | class SchemaHash 7 | extend Forwardable 8 | 9 | # @return [Hash] 10 | attr_reader :data 11 | # @return [String] 12 | attr_reader :filename 13 | 14 | def_delegator :@data, :[] 15 | def_delegator :@data, :[]= 16 | def_delegator :@data, :delete 17 | def_delegator :@data, :each 18 | 19 | # @param [Hash] data 20 | # @param [Hash] options 21 | def initialize(data, options = {}) 22 | @data = data 23 | @filename = options.fetch(:filename, "") 24 | end 25 | 26 | # @param [Prmd::SchemaHash] other 27 | # @return [self] 28 | def initialize_copy(other) 29 | super 30 | @data = other.data.dup 31 | @filename = other.filename.dup 32 | end 33 | 34 | # @param [String] key 35 | # @return [self] 36 | def fetch(key) 37 | @data.fetch(key) { abort "Missing key #{key} in #{filename}" } 38 | end 39 | 40 | # @return [Hash] 41 | def to_h 42 | @data.dup 43 | end 44 | 45 | protected :data 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/prmd/core_ext/optparse.rb: -------------------------------------------------------------------------------- 1 | require "optparse" 2 | 3 | # Extension of the standard library OptionParser 4 | class OptionParser 5 | alias_method :to_str, :to_s 6 | end 7 | -------------------------------------------------------------------------------- /lib/prmd/hash_helpers.rb: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | module Prmd 3 | # Hash helper methods 4 | # 5 | # @api private 6 | module HashHelpers 7 | # Attempts to convert all keys in the hash to a Symbol. 8 | # This operation is recursive with subhashes 9 | # 10 | # @param [Hash] hash 11 | # @return [Hash] 12 | def self.deep_symbolize_keys(hash) 13 | deep_transform_keys(hash) do |key| 14 | if key.respond_to?(:to_sym) 15 | key.to_sym 16 | else 17 | key 18 | end 19 | end 20 | end 21 | 22 | # Think of this as hash.keys.map! { |key| }, that actually maps recursively. 23 | # 24 | # @param [Hash] hash 25 | # @return [Hash] 26 | # @yield [Object] key 27 | def self.deep_transform_keys(hash, &block) 28 | result = {} 29 | hash.each do |key, value| 30 | new_key = yield(key) 31 | new_value = value 32 | new_value = deep_transform_keys(value, &block) if value.is_a?(Hash) 33 | result[new_key] = new_value 34 | end 35 | result 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/prmd/link.rb: -------------------------------------------------------------------------------- 1 | require "ostruct" 2 | 3 | module Prmd 4 | class Link 5 | def initialize(link_schema) 6 | @link_schema = link_schema 7 | end 8 | 9 | def required_and_optional_parameters 10 | @params = { required: {}, optional: {} } 11 | recurse_properties(Schema.new(@link_schema["schema"]), "") 12 | [@params[:required], @params[:optional]] 13 | end 14 | 15 | private 16 | 17 | def recurse_properties(schema, prefix = "", parent_required = false) 18 | return unless schema.has_properties? 19 | 20 | schema.properties.keys.each do |prop_name| 21 | prop = schema.properties[prop_name] 22 | pref = "#{prefix}#{prop_name}" 23 | required = parent_required || schema.property_is_required?(prop_name) 24 | 25 | handle_property(prop, pref, required) 26 | end 27 | end 28 | 29 | def handle_property(property, prefix, required = false) 30 | if property_is_object?(property["type"]) 31 | recurse_properties(Schema.new(property), "#{prefix}:", required) 32 | else 33 | categorize_parameter(prefix, property, required) 34 | end 35 | end 36 | 37 | def property_is_object?(type) 38 | return false unless type 39 | type == "object" || type.include?("object") 40 | end 41 | 42 | def categorize_parameter(name, param, required = false) 43 | @params[(required ? :required : :optional)][name] = param 44 | end 45 | 46 | class Schema < OpenStruct 47 | def has_properties? 48 | properties && !properties.empty? 49 | end 50 | 51 | def property_is_required?(property_name) 52 | return false unless required 53 | required.include?(property_name) 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/prmd/load_schema_file.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | require "json" 3 | require_relative "multi_loader" 4 | 5 | module Prmd # :nodoc: 6 | # Attempts to load either a json or yaml file, the type is determined by 7 | # filename extension. 8 | # 9 | # @param [String] filename 10 | # @return [Object] data 11 | def self.load_schema_file(filename) 12 | Prmd::MultiLoader.load_file(filename) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/prmd/multi_loader.rb: -------------------------------------------------------------------------------- 1 | require_relative "multi_loader/json" 2 | require_relative "multi_loader/yaml" 3 | -------------------------------------------------------------------------------- /lib/prmd/multi_loader/json.rb: -------------------------------------------------------------------------------- 1 | require_relative "loader" 2 | require "json" 3 | 4 | module Prmd # :nodoc: 5 | module MultiLoader # :nodoc: 6 | # JSON MultiLoader 7 | module Json 8 | extend Prmd::MultiLoader::Loader 9 | 10 | # @see (Prmd::MultiLoader::Loader#load_data) 11 | def self.load_data(data) 12 | ::JSON.load(data) 13 | end 14 | 15 | # register this loader for all .json files 16 | extensions ".json" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/prmd/multi_loader/loader.rb: -------------------------------------------------------------------------------- 1 | module Prmd # :nodoc: 2 | module MultiLoader # :nodoc: 3 | # Exception raised when a extension loader cannot be found. 4 | class LoaderNotFound < StandardError 5 | end 6 | 7 | # @return [Hash] 8 | @file_extensions = {} 9 | 10 | class << self 11 | attr_accessor :file_extensions 12 | end 13 | 14 | # Attempts to autoload a Loader named +name+ 15 | # 16 | # @param [String] 17 | # @return [Boolean] load success 18 | def self.autoload_loader(name) 19 | # extension names are preceeded with a . 20 | # TODO. probably just remove the first . 21 | loader_name = name.gsub(".", "") 22 | require "prmd/multi_loader/#{loader_name}" 23 | true 24 | rescue 25 | false 26 | end 27 | 28 | # Locates and returns a loader for the given +ext+ 29 | # If no extension is found the first time, MultiLoader will attempt 30 | # to load one of the same name. 31 | # 32 | # @param [String] ext 33 | # @return [Prmd::MultiLoader::Loader] 34 | # @eg 35 | # # by default, Prmd does not load the TOML Loader 36 | # MultiLoader.loader('.toml') 37 | # # this will check the loaders the first time and find that 38 | # # there is no Loader for toml, it will then use the ::autoload_loader 39 | # # to locate a Loader named "prmd/multi_loader/toml" 40 | def self.loader(name) 41 | tried_autoload = false 42 | begin 43 | @file_extensions.fetch(name) 44 | rescue KeyError 45 | if tried_autoload 46 | raise LoaderNotFound, "Loader for extension (#{name}) was not found." 47 | else 48 | autoload_loader(name) 49 | tried_autoload = true 50 | retry 51 | end 52 | end 53 | end 54 | 55 | # @param [String] ext 56 | # @param [String] data 57 | # @eg 58 | # Prmd::MultiLoader.load_data('.json', json_string) 59 | def self.load_data(ext, data) 60 | loader(ext).load_data(data) 61 | end 62 | 63 | # @param [String] ext name of the loader also the extension of the stream 64 | # @param [IO] stream 65 | # @eg 66 | # Prmd::MultiLoader.load_stream('.json', io) 67 | def self.load_stream(ext, stream) 68 | loader(ext).load_stream(stream) 69 | end 70 | 71 | # Shortcut for loading any supported file 72 | # 73 | # @param [String] ext 74 | # @param [String] filename 75 | # @eg 76 | # Prmd::MultiLoader.load_file('my_file.json') 77 | def self.load_file(filename) 78 | ext = File.extname(filename) 79 | loader(ext).load_file(filename) 80 | end 81 | 82 | # Base Loader module used to extend all other loaders 83 | module Loader 84 | # Using the loader, parse or do whatever magic the loader does to the 85 | # string to get back data. 86 | # 87 | # @param [String] data 88 | # @return [Object] 89 | # @abstract 90 | def load_data(data) 91 | # overwrite in children 92 | end 93 | 94 | # Load a stream 95 | # 96 | # @param [IO] stream 97 | # @return [Object] 98 | # @eg 99 | # my_io = File.open('my_file.ext', 'r') 100 | # my_loader.load_stream(my_io) 101 | def load_stream(stream) 102 | load_data(stream.read) 103 | end 104 | 105 | # Load a file given a +filename+ 106 | # 107 | # @param [String] filename 108 | # @return [Object] 109 | # @eg 110 | # my_loader.load_file('my_file.ext') 111 | def load_file(filename) 112 | File.open(filename, "r") { |f| return load_stream(f) } 113 | end 114 | 115 | # Register the loader to the +args+ extensions 116 | # 117 | # @param [Array] args 118 | # @eg extensions '.json' 119 | def extensions(*args) 120 | args.each do |file| 121 | Prmd::MultiLoader.file_extensions[file] = self 122 | end 123 | end 124 | 125 | private :extensions 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/prmd/multi_loader/toml.rb: -------------------------------------------------------------------------------- 1 | require_relative "loader" 2 | require "toml" 3 | 4 | module Prmd # :nodoc: 5 | module MultiLoader # :nodoc: 6 | # TOML MultiLoader 7 | module Toml 8 | extend Prmd::MultiLoader::Loader 9 | 10 | # @see (Prmd::MultiLoader::Loader#load_data) 11 | def self.load_data(data) 12 | ::TOML.load(data) 13 | end 14 | 15 | # register this loader for all .toml files 16 | extensions ".toml" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/prmd/multi_loader/yajl.rb: -------------------------------------------------------------------------------- 1 | require_relative "loader" 2 | require "yajl" 3 | 4 | module Prmd # :nodoc: 5 | module MultiLoader # :nodoc: 6 | # JSON MultiLoader using Yajl 7 | module Yajl 8 | extend Prmd::MultiLoader::Loader 9 | 10 | # @see (Prmd::MultiLoader::Loader#load_data) 11 | def self.load_data(data) 12 | ::Yajl::Parser.parse(data) 13 | end 14 | 15 | # register this loader for all .json files 16 | extensions ".json" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/prmd/multi_loader/yaml.rb: -------------------------------------------------------------------------------- 1 | require_relative "loader" 2 | require "yaml" 3 | 4 | module Prmd # :nodoc: 5 | module MultiLoader # :nodoc: 6 | # YAML MultiLoader 7 | module Yaml 8 | extend Prmd::MultiLoader::Loader 9 | 10 | # @see (Prmd::MultiLoader::Loader#load_data) 11 | def self.load_data(data) 12 | ::YAML.load(data) 13 | end 14 | 15 | # register this loader for all .yaml and .yml files 16 | extensions ".yaml", ".yml" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/prmd/multi_loader/yml.rb: -------------------------------------------------------------------------------- 1 | # alias for yaml 2 | require_relative "yaml" 3 | -------------------------------------------------------------------------------- /lib/prmd/rake_tasks/base.rb: -------------------------------------------------------------------------------- 1 | require "rake" 2 | require "rake/tasklib" 3 | 4 | # :nodoc: 5 | module Prmd 6 | # :nodoc: 7 | module RakeTasks 8 | # Common class for Prmd rake tasks 9 | # 10 | # @api private 11 | class Base < Rake::TaskLib 12 | # The name of the task 13 | # @return [String] the task name 14 | attr_accessor :name 15 | 16 | # Options to pass to command 17 | # @return [Hash] the options passed to the Prmd command 18 | attr_accessor :options 19 | 20 | # Creates a new task with name +name+. 21 | # 22 | # @param [Hash] options 23 | # .option [String, Symbol] name the name of the rake task 24 | # .option [String, Symbol] options options to pass to the Prmd command 25 | def initialize(options = {}) 26 | @name = options.fetch(:name, default_name) 27 | @options = options.fetch(:options) { {} } 28 | 29 | yield self if block_given? 30 | 31 | define 32 | end 33 | 34 | private 35 | 36 | # This method will be removed in the future 37 | # @api private 38 | def legacy_parameters(*args) 39 | if args.size == 0 40 | {} 41 | else 42 | arg, = *args 43 | case arg 44 | when String, Symbol 45 | warn "#{self.class}.new(name) has been deprecated, use .new(name: name) instead" 46 | { name: arg } 47 | else 48 | arg 49 | end 50 | end 51 | end 52 | 53 | # Default name of the rake task 54 | # 55 | # @return [Symbol] 56 | def default_name 57 | :base 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/prmd/rake_tasks/combine.rb: -------------------------------------------------------------------------------- 1 | require "prmd/commands/combine" 2 | require "prmd/rake_tasks/base" 3 | 4 | # :nodoc: 5 | module Prmd 6 | # :nodoc: 7 | module RakeTasks 8 | # Schema combine rake task 9 | # 10 | # @example 11 | # Prmd::RakeTasks::Combine.new do |t| 12 | # t.options[:meta] = 'schema/meta.json' 13 | # t.paths << 'schema/schemata/api' 14 | # t.output_file = 'schema/api.json' 15 | # end 16 | class Combine < Base 17 | # 18 | # @return [Array] list of paths 19 | attr_accessor :paths 20 | 21 | # target file the combined result should be written 22 | # @return [String>] target filename 23 | attr_accessor :output_file 24 | 25 | # Creates a new task with name +name+. 26 | # 27 | # @overload initialize(name) 28 | # @param [String] 29 | # @overload initialize(options) 30 | # @param [Hash] options 31 | # .option [String] output_file 32 | # .option [Array] paths 33 | def initialize(*, &) 34 | options = legacy_parameters(*) 35 | @paths = options.fetch(:paths) { [] } 36 | @output_file = options[:output_file] 37 | super(options, &) 38 | end 39 | 40 | private 41 | 42 | # Default name of the rake task 43 | # 44 | # @return [Symbol] 45 | def default_name 46 | :combine 47 | end 48 | 49 | protected 50 | 51 | # Defines the rake task 52 | # @return [void] 53 | def define 54 | desc "Combine schemas" unless Rake.application.last_description 55 | task(name) do 56 | result = Prmd.combine(paths, options) 57 | if output_file 58 | File.write(output_file, result) 59 | end 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/prmd/rake_tasks/doc.rb: -------------------------------------------------------------------------------- 1 | require "prmd/commands/render" 2 | require "prmd/rake_tasks/base" 3 | require "prmd/load_schema_file" 4 | require "prmd/url_generator" 5 | require "prmd/template" 6 | require "prmd/schema" 7 | require "prmd/link" 8 | require_relative "../hash_helpers" 9 | 10 | # :nodoc: 11 | module Prmd 12 | # :nodoc: 13 | module RakeTasks 14 | # Documentation rake task 15 | # 16 | # @example 17 | # Prmd::RakeTasks::Doc.new do |t| 18 | # t.files = { 'schema/api.json' => 'schema/api.md' } 19 | # end 20 | class Doc < Base 21 | # Schema files that should be rendered 22 | # @return [Array, Hash] list of files 23 | attr_accessor :files 24 | 25 | attr_accessor :toc 26 | 27 | # Creates a new task with name +name+. 28 | # 29 | # @overload initialize(name) 30 | # @param [String] 31 | # @overload initialize(options) 32 | # @param [Hash] options 33 | # .option [Array, Hash] files schema files 34 | def initialize(*, &) 35 | options = legacy_parameters(*) 36 | @files = options.fetch(:files) { [] } 37 | super(options, &) 38 | if @options[:settings].is_a? String 39 | settings = Prmd.load_schema_file(@options[:settings]) 40 | @options.merge! HashHelpers.deep_symbolize_keys(settings) 41 | end 42 | @options[:template] ||= Prmd::Template.template_dirname 43 | end 44 | 45 | private 46 | 47 | # Default name of the rake task 48 | # 49 | # @return [Symbol] 50 | def default_name 51 | :doc 52 | end 53 | 54 | # Render file to markdown 55 | # 56 | # @param [String] filename 57 | # @return (see Prmd.render) 58 | def render_file(filename) 59 | data = Prmd.load_schema_file(filename) 60 | schema = Prmd::Schema.new(data) 61 | Prmd.render(schema, options) 62 | end 63 | 64 | # Render +infile+ to +outfile+ 65 | # 66 | # @param [String] infile 67 | # @param [String] outfile 68 | # @return [void] 69 | def render_to_file(infile, outfile) 70 | result = render_file(infile) 71 | if outfile 72 | File.write(outfile, result) 73 | end 74 | end 75 | 76 | protected 77 | 78 | # Defines the rake task 79 | # @return [void] 80 | def define 81 | desc "Generate documentation" unless Rake.application.last_description 82 | task(name) do 83 | if files.is_a?(Hash) 84 | files.each do |infile, outfile| 85 | render_to_file(infile, outfile) 86 | end 87 | else 88 | files.each do |infile| 89 | render_to_file(infile, infile.ext("md")) 90 | end 91 | end 92 | end 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/prmd/rake_tasks/verify.rb: -------------------------------------------------------------------------------- 1 | require "prmd/commands/verify" 2 | require "prmd/rake_tasks/base" 3 | require "prmd/load_schema_file" 4 | 5 | # :nodoc: 6 | module Prmd 7 | # :nodoc: 8 | module RakeTasks 9 | # Schema Verify rake task 10 | # 11 | # @example 12 | # Prmd::RakeTasks::Verify.new do |t| 13 | # t.files << 'schema/api.json' 14 | # end 15 | class Verify < Base 16 | # Schema files that should be verified 17 | # @return [Array] list of files 18 | attr_accessor :files 19 | 20 | # Creates a new task with name +name+. 21 | # 22 | # @overload initialize(name) 23 | # @param [String] 24 | # @overload initialize(options) 25 | # @param [Hash] options 26 | # .option [Array] files schema files to verify 27 | def initialize(*, &) 28 | options = legacy_parameters(*) 29 | @files = options.fetch(:files) { [] } 30 | super(options, &) 31 | end 32 | 33 | private 34 | 35 | # Default name of the rake task 36 | # 37 | # @return [Symbol] 38 | def default_name 39 | :verify 40 | end 41 | 42 | # Defines the rake task 43 | # 44 | # @param [String] filename 45 | # @return [Array] list of errors produced 46 | def verify_file(filename) 47 | data = Prmd.load_schema_file(filename) 48 | errors = Prmd.verify(data) 49 | unless errors.empty? 50 | errors.map! { |error| "#{filename}: #{error}" } if filename 51 | errors.each { |error| warn(error) } 52 | end 53 | errors 54 | end 55 | 56 | protected 57 | 58 | # Defines the rake task 59 | # @return [void] 60 | def define 61 | desc "Verify schemas" unless Rake.application.last_description 62 | task(name) do 63 | all_errors = [] 64 | files.each do |filename| 65 | all_errors.concat(verify_file(filename)) 66 | end 67 | fail unless all_errors.empty? 68 | end 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/prmd/schema.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | require "yaml" 3 | 4 | # :nodoc: 5 | module Prmd 6 | # @api private 7 | DefaultExamples = { 8 | "boolean" => true, 9 | "integer" => 42, 10 | "number" => 42.0, 11 | "string" => "example", 12 | 13 | "date" => "2015-01-01", 14 | "date-time" => "2015-01-01T12:00:00Z", 15 | "email" => "username@example.com", 16 | "hostname" => "example.com", 17 | "ipv4" => "192.0.2.1", 18 | "ipv6" => "2001:DB8::1", 19 | "uuid" => "01234567-89ab-cdef-0123-456789abcdef", 20 | } 21 | 22 | # Schema object 23 | class Schema 24 | # @return [Hash] data 25 | attr_reader :data 26 | 27 | # @param [Hash] new_data 28 | def initialize(new_data = {}, options = {}) 29 | @data = convert_type_to_array(new_data, options) 30 | @schemata_examples = {} 31 | end 32 | 33 | # 34 | # @param [Object] datum 35 | # @return [Object] same type as the input object 36 | def convert_type_to_array(datum, options) 37 | case datum 38 | when Array 39 | datum.map { |element| convert_type_to_array(element, options) } 40 | when Hash 41 | if datum.key?("type") && datum["type"].is_a?(String) && !options[:type_as_string] 42 | datum["type"] = [*datum["type"]] 43 | end 44 | datum.each_with_object({}) do |(k, v), hash| 45 | hash[k] = if k != "example" 46 | convert_type_to_array(v, options) 47 | else 48 | v 49 | end 50 | end 51 | else 52 | datum 53 | end 54 | end 55 | 56 | # @param [String] key 57 | # @return [Object] 58 | def [](key) 59 | @data[key] 60 | end 61 | 62 | # @param [String] key 63 | # @param [Object] value 64 | def []=(key, value) 65 | @data[key] = value 66 | end 67 | 68 | # Merge schema data with provided schema 69 | # 70 | # @param [Hash, Prmd::Schema] schema 71 | # @return [void] 72 | def merge!(schema) 73 | if schema.is_a?(Schema) 74 | @data.merge!(schema.data) 75 | else 76 | @data.merge!(schema) 77 | end 78 | end 79 | 80 | # 81 | # @param [Hash, String] reference 82 | def dereference(reference) 83 | if reference.is_a?(Hash) 84 | if reference.key?("$ref") 85 | value = reference.dup 86 | key = value.delete("$ref") 87 | else 88 | return [nil, reference] # no dereference needed 89 | end 90 | else 91 | key, value = reference, {} 92 | end 93 | begin 94 | datum = @data 95 | key.gsub(/[^#]*#\//, "").split("/").each do |fragment| 96 | datum = datum[fragment] 97 | end 98 | # last dereference will have nil key, so compact it out 99 | # [-2..-1] should be the final key reached before deref 100 | dereferenced_key, dereferenced_value = dereference(datum) 101 | [ 102 | [key, dereferenced_key].compact.last, 103 | [dereferenced_value, value].inject({}, &:merge), 104 | ] 105 | rescue => error 106 | warn("Failed to dereference `#{key}`") 107 | raise error 108 | end 109 | end 110 | 111 | # @param [Hash] value 112 | def schema_value_example(value) 113 | if value.key?("example") 114 | value["example"] 115 | elsif value.key?("anyOf") 116 | id_ref = value["anyOf"].find do |ref| 117 | ref["$ref"] && ref["$ref"].split("/").last == "id" 118 | end 119 | ref = id_ref || value["anyOf"].first 120 | schema_example(ref) 121 | elsif value.key?("allOf") 122 | value["allOf"].map { |ref| schema_example(ref) }.reduce({}, &:merge) 123 | elsif value.key?("properties") # nested properties 124 | schema_example(value) 125 | elsif value.key?("items") # array of objects 126 | _, items = dereference(value["items"]) 127 | if value["items"].key?("example") 128 | if items["example"].is_a?(Array) 129 | items["example"] 130 | else 131 | [items["example"]] 132 | end 133 | else 134 | [schema_example(items)] 135 | end 136 | elsif value.key?("enum") 137 | value["enum"][0] 138 | elsif DefaultExamples.key?(value["format"]) 139 | DefaultExamples[value["format"]] 140 | elsif DefaultExamples.key?(value["type"][0]) 141 | DefaultExamples[value["type"][0]] 142 | end 143 | end 144 | 145 | # @param [Hash, String] schema 146 | def schema_example(schema) 147 | _, dff_schema = dereference(schema) 148 | 149 | if dff_schema.key?("example") 150 | dff_schema["example"] 151 | elsif dff_schema.key?("properties") 152 | example = {} 153 | dff_schema["properties"].each do |key, value| 154 | _, value = dereference(value) 155 | example[key] = schema_value_example(value) 156 | end 157 | example 158 | elsif dff_schema.key?("items") 159 | schema_value_example(dff_schema) 160 | end 161 | end 162 | 163 | # @param [String] schemata_id 164 | def schemata_example(schemata_id) 165 | _, schema = dereference("#/definitions/#{schemata_id}") 166 | @schemata_examples[schemata_id] ||= schema_example(schema) 167 | end 168 | 169 | # Retrieve this schema's href 170 | # 171 | # @return [String, nil] 172 | def href 173 | (@data["links"] && @data["links"].find { |link| link["rel"] == "self" } || {})["href"] 174 | end 175 | 176 | # Convert Schema to JSON 177 | # 178 | # @return [String] 179 | def to_json 180 | new_json = JSON.pretty_generate(@data) 181 | # nuke empty lines 182 | new_json.split("\n").reject(&:empty?).join("\n") + "\n" 183 | end 184 | 185 | # Convert Schema to YAML 186 | # 187 | # @return [String] 188 | def to_yaml 189 | YAML.dump(@data) 190 | end 191 | 192 | # Convert Schema to String 193 | # 194 | # @return [String] 195 | def to_s 196 | to_json 197 | end 198 | 199 | private :convert_type_to_array 200 | protected :data 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /lib/prmd/template.rb: -------------------------------------------------------------------------------- 1 | require "erubis" 2 | require "json" 3 | 4 | # :nodoc: 5 | module Prmd 6 | # Template management 7 | # 8 | # @api private 9 | class Template 10 | @cache = {} 11 | 12 | # @return [String] location of the prmd templates directory 13 | def self.template_dirname 14 | File.join(File.dirname(__FILE__), "templates") 15 | end 16 | 17 | # @param [String] args 18 | # @return [String] path in prmd's template directory 19 | def self.template_path(*) 20 | File.expand_path(File.join(*), template_dirname) 21 | end 22 | 23 | # Clear internal template cache 24 | # 25 | # @return [void] 26 | def self.clear_cache 27 | @cache.clear 28 | end 29 | 30 | # Attempts to load a template from the given path and base, if the template 31 | # was previously loaded, the cached template is returned instead 32 | # 33 | # @param [String] path 34 | # @param [String] base 35 | # @return [Erubis::Eruby] eruby template 36 | def self.load(path, base) 37 | @cache[[path, base]] ||= begin 38 | fallback = template_path 39 | 40 | resolved = File.join(base, path) 41 | unless File.exist?(resolved) 42 | resolved = File.join(fallback, path) 43 | end 44 | 45 | Erubis::Eruby.new(File.read(resolved), filename: resolved) 46 | end 47 | end 48 | 49 | # 50 | # @param [String] path 51 | # @param [String] base 52 | # @return (see .load) 53 | def self.load_template(path, base) 54 | load(path, base) 55 | end 56 | 57 | # Render a template given args or block. 58 | # args and block are passed to the template 59 | # 60 | # @param [String] path 61 | # @param [String] base 62 | # @return [String] result from template render 63 | def self.render(path, base, *, &) 64 | load_template(path, base).result(*, &) 65 | end 66 | 67 | # Load a JSON file from prmd's templates directory. 68 | # These files are not cached and are intended to be loaded on demand. 69 | # 70 | # @param [String] filename 71 | # @return [Object] data 72 | def self.load_json(filename) 73 | JSON.parse(File.read(template_path(filename))) 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/prmd/templates/combine_head.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://interagent.github.io/interagent-hyper-schema", 3 | "type": ["object"], 4 | "definitions": {}, 5 | "properties": {} 6 | } 7 | -------------------------------------------------------------------------------- /lib/prmd/templates/init_default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/hyper-schema", 3 | "title": "FIXME", 4 | "description": "FIXME", 5 | "stability": "prototype", 6 | "strictProperties": true, 7 | "type": ["object"], 8 | "definitions": {}, 9 | "links": [], 10 | "properties": {} 11 | } 12 | -------------------------------------------------------------------------------- /lib/prmd/templates/init_resource.json.erb: -------------------------------------------------------------------------------- 1 | { 2 | "id": "schemata/<%= resource %>", 3 | "title": "FIXME - <%= resource.capitalize %>", 4 | "definitions": { 5 | "id": { 6 | "description": "unique identifier of <%= resource %>", 7 | "readOnly": true, 8 | "format": "uuid", 9 | "type": ["string"] 10 | }, 11 | "name": { 12 | "description": "unique name of <%= resource %>", 13 | "readOnly": true, 14 | "type": ["string"] 15 | }, 16 | "identity": { 17 | "anyOf": [ 18 | { 19 | "$ref": "/schemata/<%= resource %>#/definitions/id" 20 | }, 21 | { 22 | "$ref": "/schemata/<%= resource %>#/definitions/name" 23 | } 24 | ] 25 | }, 26 | "created_at": { 27 | "description": "when <%= resource %> was created", 28 | "format": "date-time", 29 | "type": ["string"] 30 | }, 31 | "updated_at": { 32 | "description": "when <%= resource %> was updated", 33 | "format": "date-time", 34 | "type": ["string"] 35 | } 36 | }, 37 | "properties": { 38 | "created_at": { 39 | "$ref": "/schemata/<%= resource %>#/definitions/created_at" 40 | }, 41 | "id": { 42 | "$ref": "/schemata/<%= resource %>#/definitions/id" 43 | }, 44 | "name": { 45 | "$ref": "/schemata/<%= resource %>#/definitions/name" 46 | }, 47 | "updated_at": { 48 | "$ref": "/schemata/<%= resource %>#/definitions/updated_at" 49 | } 50 | }, 51 | "links": [ 52 | { 53 | "description": "Create a new <%= resource %>.", 54 | "href": "/<%= resource %>s", 55 | "method": "POST", 56 | "rel": "create", 57 | "schema": { 58 | "properties": {}, 59 | "type": ["object"] 60 | }, 61 | "title": "Create" 62 | }, 63 | { 64 | "description": "Delete an existing <%= resource %>.", 65 | "href": "/<%= resource %>s/{(%2Fschemata%2F<%= resource %>%23%2Fdefinitions%2Fidentity)}", 66 | "method": "DELETE", 67 | "rel": "destroy", 68 | "title": "Delete" 69 | }, 70 | { 71 | "description": "Info for existing <%= resource %>.", 72 | "href": "/<%= resource %>s/{(%2Fschemata%2F<%= resource %>%23%2Fdefinitions%2Fidentity)}", 73 | "method": "GET", 74 | "rel": "self", 75 | "title": "Info" 76 | }, 77 | { 78 | "description": "List existing <%= resource %>s.", 79 | "href": "/<%= resource %>s", 80 | "method": "GET", 81 | "rel": "instances", 82 | "title": "List" 83 | }, 84 | { 85 | "description": "Update an existing <%= resource %>.", 86 | "href": "/<%= resource %>s/{(%2Fschemata%2F<%= resource %>%23%2Fdefinitions%2Fidentity)}", 87 | "method": "PATCH", 88 | "rel": "update", 89 | "schema": { 90 | "properties": {}, 91 | "type": ["object"] 92 | }, 93 | "title": "Update" 94 | }<% if parent %>, 95 | { 96 | "description": "List existing <%= resource %>s for existing <%= parent %>.", 97 | "href": "/<%= parent %>s/{(%2Fschemata%2F<%= parent %>%23%2Fdefinitions%2Fidentity)}/<%= resource %>s", 98 | "method": "GET", 99 | "rel": "instances", 100 | "title": "List" 101 | }<% end %> 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /lib/prmd/templates/link_schema_properties.md.erb: -------------------------------------------------------------------------------- 1 | | Name | Type | Description | Example | 2 | | ------- | ------- | ------- | ------- | 3 | <%- extract_attributes(schema, params).each do |(key, type, description, example)| %> 4 | | **<%= key %>** | *<%= type %>* | <%= description %> | <%= example %> | 5 | <%- end %> 6 | -------------------------------------------------------------------------------- /lib/prmd/templates/schema.erb: -------------------------------------------------------------------------------- 1 | <%- if options[:doc][:toc] -%> 2 | <%= 3 | Prmd::Template::load('table_of_contents.erb', options[:template]).result({ 4 | options: options, 5 | schema: schema 6 | }) 7 | %> 8 | <%- end -%> 9 | <%= 10 | schemata_template = Prmd::Template::load('schemata.md.erb', options[:template]) 11 | 12 | schema['properties'].keys.sort.map do |key| 13 | resource, property = key, schema['properties'][key] 14 | begin 15 | _, schemata = schema.dereference(property) 16 | schemata_template.result({ 17 | options: options, 18 | resource: resource, 19 | schema: schema, 20 | schemata: schemata 21 | }) 22 | rescue => e 23 | $stdout.puts("Error in resource: #{resource}") 24 | raise e 25 | end 26 | end.join("\n") 27 | %> 28 | -------------------------------------------------------------------------------- /lib/prmd/templates/schemata.md.erb: -------------------------------------------------------------------------------- 1 | <%- 2 | Prmd::Template.render('schemata/helper.erb', options[:template], { 3 | options: options, 4 | resource: resource, 5 | schema: schema, 6 | schemata: schemata 7 | }) 8 | 9 | title = schemata['title'].split(' - ', 2).last 10 | -%> 11 | <%- unless options[:doc][:disable_title_and_description] %> 12 | 13 | ## <%= title %> 14 | 15 | <%- if schemata['stability'] && !schemata['stability'].empty? %> 16 | Stability: `<%= schemata['stability'] %>` 17 | <%- end -%> 18 | 19 | <%= schemata['description'] %> 20 | <%- end -%> 21 | 22 | <%- if schemata['properties'] && !schemata['properties'].empty? %> 23 | 24 | ### Attributes 25 | 26 |
27 | Details 28 | 29 | 30 | | Name | Type | Description | Example | 31 | | ------- | ------- | ------- | ------- | 32 | <%- refs = extract_schemata_refs(schema, schemata['properties']).map {|v| v && v.split("/")} %> 33 | <%- extract_attributes(schema, schemata['properties']).each_with_index do |(key, type, description, example), i| %> 34 | <%- if refs[i] && refs[i][1] == "definitions" && refs[i][2] != resource %> 35 | <%- name = '[%s](#%s)' % [key, 'resource-' + refs[i][2]] %> 36 | <%- else %> 37 | <%- name = key %> 38 | <%- end %> 39 | | **<%= name %>** | *<%= type %>* | <%= description %> | <%= example %> | 40 | <%- end %> 41 | 42 |
43 | 44 | <%- end %> 45 | <%- (schemata['links'] || []).each do |link, datum| %> 46 | <%= 47 | Prmd::Template.render('schemata/link.md.erb', options[:template], { 48 | options: options, 49 | resource: resource, 50 | schema: schema, 51 | schemata: schemata, 52 | link: link, 53 | title: title 54 | }) 55 | %> 56 | <%- end -%> 57 | -------------------------------------------------------------------------------- /lib/prmd/templates/schemata/helper.erb: -------------------------------------------------------------------------------- 1 | <%- 2 | def extract_attributes(schema, properties) 3 | attributes = [] 4 | 5 | _, properties = schema.dereference(properties) 6 | 7 | properties.each do |key, value| 8 | # found a reference to another element: 9 | _, value = schema.dereference(value) 10 | 11 | # include top level reference to nested things, when top level is nullable 12 | if value.has_key?('type') && value['type'].include?('null') && (value.has_key?('items') || value.has_key?('properties')) 13 | attributes << build_attribute(schema, key, value) 14 | end 15 | 16 | if value.has_key?('anyOf') 17 | descriptions = [] 18 | examples = [] 19 | 20 | anyof = value['anyOf'] 21 | 22 | anyof.each do |ref| 23 | _, nested_field = schema.dereference(ref) 24 | descriptions << nested_field['description'] if nested_field['description'] 25 | examples << nested_field['example'] if nested_field['example'] 26 | end 27 | 28 | # avoid repetition :} 29 | description = if descriptions.size > 1 30 | descriptions.first.gsub!(/ of (this )?.*/, "") 31 | descriptions[1..-1].map { |d| d.gsub!(/unique /, "") } 32 | [descriptions[0...-1].join(", "), descriptions.last].join(" or ") 33 | else 34 | description = descriptions.last 35 | end 36 | 37 | example = [*examples].map { |e| "`#{e.to_json}`" }.join(" or ") 38 | attributes << [key, "string", description, example] 39 | 40 | # found a nested object 41 | elsif value['properties'] 42 | nested = extract_attributes(schema, value['properties']) 43 | nested.each do |attribute| 44 | attribute[0] = "#{key}:#{attribute[0]}" 45 | end 46 | attributes.concat(nested) 47 | 48 | elsif array_with_nested_objects?(value['items']) 49 | if value['items']['properties'] 50 | nested = extract_attributes(schema, value['items']['properties']) 51 | nested.each do |attribute| 52 | attribute[0] = "#{key}/#{attribute[0]}" 53 | end 54 | attributes.concat(nested) 55 | end 56 | if value['items']['oneOf'] 57 | value['items']['oneOf'].each_with_index do |oneof, index| 58 | ref, oneof_definition = schema.dereference(oneof) 59 | oneof_name = ref ? ref.split('/').last : index 60 | nested = extract_attributes(schema, oneof_definition['properties']) 61 | nested.each do |attribute| 62 | attribute[0] = "#{key}/[#{oneof_name.upcase}].#{attribute[0]}" 63 | end 64 | attributes.concat(nested) 65 | end 66 | end 67 | 68 | # just a regular attribute 69 | else 70 | attributes << build_attribute(schema, key, value) 71 | end 72 | end 73 | attributes.map! { |key, type, description, example| 74 | if example.nil? && Prmd::DefaultExamples.key?(type) 75 | example = "`%s`" % Prmd::DefaultExamples[type].to_json 76 | end 77 | [key, type, description, example] 78 | } 79 | return attributes.sort 80 | end 81 | 82 | def extract_schemata_refs(schema, properties) 83 | ret = [] 84 | properties.keys.sort.each do |key| 85 | value = properties[key] 86 | ref, value = schema.dereference(value) 87 | if value['properties'] 88 | refs = extract_schemata_refs(schema, value['properties']) 89 | elsif value['items'] && value['items']['properties'] 90 | refs = extract_schemata_refs(schema, value['items']['properties']) 91 | else 92 | refs = [ref] 93 | end 94 | if value.has_key?('type') && value['type'].include?('null') && (value.has_key?('items') || value.has_key?('properties')) 95 | # A nullable object usually isn't a reference to another schema. It's 96 | # either not a reference at all, or it's a reference within the same 97 | # schema. Instead, the definition of the nullable object might contain 98 | # references to specific properties. 99 | # 100 | # If all properties refer to the same schema, we'll use that as the 101 | # reference. This might even overwrite an actual, intra-schema 102 | # reference. 103 | 104 | l = refs.map {|v| v && v.split("/")[0..2]} 105 | if l.uniq.size == 1 && l[0] != nil 106 | ref = l[0].join("/") 107 | end 108 | ret << ref 109 | end 110 | ret.concat(refs) 111 | end 112 | ret 113 | end 114 | 115 | def build_attribute(schema, key, value) 116 | description = value['description'] || "" 117 | if value['default'] 118 | description += "
**default:** `#{value['default'].to_json}`" 119 | end 120 | 121 | if value['minimum'] || value['maximum'] 122 | description += "
**Range:** `" 123 | if value['minimum'] 124 | comparator = value['exclusiveMinimum'] ? "<" : "<=" 125 | description += "#{value['minimum'].to_json} #{comparator} " 126 | end 127 | description += "value" 128 | if value['maximum'] 129 | comparator = value['exclusiveMaximum'] ? "<" : "<=" 130 | description += " #{comparator} #{value['maximum'].to_json}" 131 | end 132 | description += "`" 133 | end 134 | 135 | if value['enum'] 136 | description += '
**one of:**' + [*value['enum']].map { |e| "`#{e.to_json}`" }.join(" or ") 137 | end 138 | 139 | if value['pattern'] 140 | # Prevent the pipe regex pattern characters from being interpreted 141 | # as markdown table. 142 | # Prevent instances of []() in regexes from being rendered as 143 | # markdown links. 144 | # Prevent patterns with * being rendered as having emphasis 145 | # or being an unordered list. 146 | pattern = value['pattern'] 147 | .gsub(/\|/, '|') 148 | .gsub(/\[/, '[') 149 | .gsub(/\*/, '*') 150 | 151 | description += "
**pattern:**
#{pattern}
" 152 | end 153 | 154 | if value['minLength'] || value['maxLength'] 155 | description += "
**Length:** `" 156 | if value['minLength'] 157 | description += "#{value['minLength'].to_json}" 158 | end 159 | unless value['minLength'] == value['maxLength'] 160 | if value['maxLength'] 161 | unless value['minLength'] 162 | description += "0" 163 | end 164 | description += "..#{value['maxLength'].to_json}" 165 | else 166 | description += "..∞" 167 | end 168 | end 169 | description += "`" 170 | end 171 | 172 | if value.has_key?('example') 173 | example = if value['example'].is_a?(Hash) && value['example'].has_key?('oneOf') 174 | value['example']['oneOf'].map { |e| "`#{e.to_json}`" }.join(" or ") 175 | else 176 | "`#{value['example'].to_json}`" 177 | end 178 | elsif (value['type'] == ['array'] && value.has_key?('items')) || value.has_key?('enum') 179 | example = "`#{schema.schema_value_example(value).to_json}`" 180 | elsif value['type'].include?('null') 181 | example = "`null`" 182 | end 183 | 184 | type = if value['type'].include?('null') 185 | 'nullable ' 186 | else 187 | '' 188 | end 189 | type += (value['format'] || (value['type'] - ['null']).join(' or ')) 190 | [key, type, description, example] 191 | end 192 | 193 | 194 | def build_link_path(schema, link) 195 | link['href'].gsub(%r|(\{\([^\)]+\)\})|) do |ref| 196 | ref = ref.gsub('%2F', '/').gsub('%23', '#').gsub(%r|[\{\(\)\}]|, '') 197 | identity_key, identity_value = schema.dereference(ref) 198 | ref_resource = identity_key.split('#/definitions/').last.split('/').first.gsub('-','_') 199 | if identity_value.has_key?('anyOf') 200 | '{' + ref_resource + '_' + identity_value['anyOf'].map {|r| r['$ref'].split('/').last}.join('_or_') + '}' 201 | else 202 | '{' + ref_resource + '_' + identity_key.split('/').last + '}' 203 | end 204 | end 205 | end 206 | 207 | def array_with_nested_objects?(items) 208 | return unless items 209 | items['properties'] || items['oneOf'] 210 | end 211 | %> 212 | -------------------------------------------------------------------------------- /lib/prmd/templates/schemata/link.md.erb: -------------------------------------------------------------------------------- 1 | <%- 2 | path = build_link_path(schema, link) 3 | response_example = link['response_example'] 4 | link_schema_properties_template = Prmd::Template.load_template('link_schema_properties.md.erb', options[:template]) 5 | -%> 6 | 7 | ### <%= title %> <%= link['title'] %> 8 | 9 |
10 | Details 11 | 12 | <%= link['description'] %> 13 | 14 | ``` 15 | <%= link['method'] %> <%= path %> 16 | ``` 17 | 18 | <%- if link.has_key?('schema') && link['schema'].has_key?('properties') %> 19 | <%- 20 | required, optional = Prmd::Link.new(link).required_and_optional_parameters 21 | %> 22 | <%- unless required.empty? %> 23 | #### Required Parameters 24 | 25 | <%= link_schema_properties_template.result(params: required, schema: schema, options: options) %> 26 | 27 | <%- end %> 28 | <%- unless optional.empty? %> 29 | #### Optional Parameters 30 | 31 | <%= link_schema_properties_template.result(params: optional, schema: schema, options: options) %> 32 | <%- end %> 33 | <%- end %> 34 | 35 | #### Curl Example 36 | 37 | <%= 38 | curl_options = options.dup 39 | http_header = link['http_header'] || {} 40 | curl_options[:http_header] = curl_options[:http_header].merge(http_header) 41 | Prmd::Template.render('schemata/link_curl_example.md.erb', File.dirname(options[:template]), { 42 | options: curl_options, 43 | resource: resource, 44 | schema: schema, 45 | schemata: schemata, 46 | link: link, 47 | path: path 48 | }) 49 | %> 50 | 51 | #### Response Example 52 | 53 | ``` 54 | <%- if response_example %> 55 | <%= response_example['head'] %> 56 | <%- else %> 57 | HTTP/1.1 <%= 58 | case link['rel'] 59 | when 'create' 60 | '201 Created' 61 | when 'empty' 62 | '202 Accepted' 63 | else 64 | '200 OK' 65 | end %> 66 | <%- end %> 67 | ``` 68 | 69 | <%- if response_example || link['rel'] != 'empty' %> 70 | ```json 71 | <%- if response_example %> 72 | <%= response_example['body'] %> 73 | <%- else %> 74 | <%- if link['rel'] == 'empty' %> 75 | <%- elsif link.has_key?('targetSchema') %> 76 | <%= JSON.pretty_generate(schema.schema_example(link['targetSchema'])) %> 77 | <%- elsif link['rel'] == 'instances' %> 78 | <%= JSON.pretty_generate([schema.schemata_example(resource)]) %> 79 | <%- else %> 80 | <%= JSON.pretty_generate(schema.schemata_example(resource)) %> 81 | <%- end %> 82 | <%- end %> 83 | ``` 84 | <%- end %> 85 | 86 |
87 | -------------------------------------------------------------------------------- /lib/prmd/templates/schemata/link_curl_example.md.erb: -------------------------------------------------------------------------------- 1 | ```bash 2 | <%- 3 | data = nil 4 | path = path.gsub(/{([^}]*)}/) {|match| '$' + match.gsub(/[{}]/, '').upcase} 5 | get_params = [] 6 | 7 | if link.has_key?('schema') 8 | data = schema.schema_example(link['schema']) 9 | 10 | if link['method'].upcase == 'GET' && !data.nil? 11 | get_params << Prmd::UrlGenerator.new({schema: schema, link: link, options: options}).url_params 12 | end 13 | end 14 | if link['method'].upcase != 'GET' 15 | options = options.dup 16 | options[:http_header] = { 'Content-Type' => options[:content_type] }.merge(options[:http_header]) 17 | end 18 | %> 19 | <%- if link['method'].upcase != 'GET' %> 20 | $ curl -n -X <%= link['method'] %> <%= schema.href %><%= path -%><%- unless options[:http_header].empty? %> \<%- end %> 21 | <%- else %> 22 | $ curl -n <%= schema.href %><%= path -%><%- unless options[:http_header].empty? %> \<%- end %> 23 | <%- end %> 24 | <%- if !data.nil? && link['method'].upcase != 'GET' %> 25 | -d '<%= JSON.pretty_generate(data) %>'<%- unless options[:http_header].empty? %> \<%- end %> 26 | <%- elsif !get_params.empty? && link['method'].upcase == 'GET' %> -G \ 27 | -d <%= get_params.join(" \\\n -d ") %><%- unless options[:http_header].empty? %> \<%- end %> 28 | <%- end %> 29 | <%- options[:http_header].each do |key, value| %> 30 | -H "<%= key %>: <%= value %>"<%- if key != options[:http_header].keys.last %> \<%- end %> 31 | <%- end %> 32 | ``` 33 | -------------------------------------------------------------------------------- /lib/prmd/templates/table_of_contents.erb: -------------------------------------------------------------------------------- 1 | <%- Prmd::Template.render('schemata/helper.erb', options[:template]) -%> 2 | ## The table of contents 3 | 4 | <% schema['properties'].keys.sort.map do |key| %> 5 | <% resource, property = key, schema['properties'][key] %> 6 | <% _, schemata = schema.dereference(property) %> 7 | - <%= schemata['title'].split(' - ', 2).last %> 8 | <% schemata.fetch('links', []).each do |l| %> 9 | - <%= l['method'] %> <%= build_link_path(schema, l) %> 10 | <% end %> 11 | <% end %> 12 | -------------------------------------------------------------------------------- /lib/prmd/url_generator.rb: -------------------------------------------------------------------------------- 1 | require_relative "url_generators/generators/default" 2 | require_relative "url_generators/generators/json" 3 | 4 | # :nodoc: 5 | module Prmd 6 | # Schema URL Generation 7 | # @api private 8 | class UrlGenerator 9 | # @param [Hash] params 10 | def initialize(params) 11 | @schema = params[:schema] 12 | @link = params[:link] 13 | @options = params.fetch(:options) 14 | end 15 | 16 | # @return [Array] 17 | def url_params 18 | klass = if @options[:doc][:url_style].downcase == "json" 19 | Generators::JSON 20 | else 21 | Generators::Default 22 | end 23 | 24 | klass.generate(schema: @schema, link: @link) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/prmd/url_generators/generators/default.rb: -------------------------------------------------------------------------------- 1 | require "cgi" 2 | 3 | # :nodoc: 4 | module Prmd 5 | # :nodoc: 6 | class UrlGenerator 7 | # :nodoc: 8 | module Generators 9 | # Default URL Generator 10 | # 11 | # @api private 12 | class Default 13 | # @param [Hash] params 14 | def self.generate(params) 15 | data = {} 16 | data.merge!(params[:schema].schema_example(params[:link]["schema"])) 17 | generate_params(data) 18 | end 19 | 20 | # @param [String] key 21 | # @param [String] prefix 22 | # @return [String] 23 | def self.param_name(key, prefix, array = false) 24 | result = if prefix 25 | "#{prefix}[#{key}]" 26 | else 27 | key 28 | end 29 | 30 | result += "[]" if array 31 | result 32 | end 33 | 34 | # @param [Hash] obj 35 | # @param [String] prefix 36 | # @return [String] 37 | def self.generate_params(obj, prefix = nil) 38 | result = [] 39 | obj.each do |key, value| 40 | if value.is_a?(Hash) 41 | newprefix = if prefix 42 | "#{prefix}[#{key}]" 43 | else 44 | key 45 | end 46 | result << generate_params(value, newprefix) 47 | elsif value.is_a?(Array) 48 | value.each do |val| 49 | result << [param_name(key, prefix, true), CGI.escape(val.to_s)].join("=") 50 | end 51 | else 52 | next unless value # ignores parameters with empty examples 53 | result << [param_name(key, prefix), CGI.escape(value.to_s)].join("=") 54 | end 55 | end 56 | result.flatten 57 | end 58 | 59 | class << self 60 | private :param_name 61 | private :generate_params 62 | end 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/prmd/url_generators/generators/json.rb: -------------------------------------------------------------------------------- 1 | require "cgi" 2 | 3 | # :nodoc: 4 | module Prmd 5 | # :nodoc: 6 | class UrlGenerator 7 | # :nodoc: 8 | module Generators 9 | # JSON URL Generator 10 | # 11 | # @api private 12 | class JSON 13 | # @param [Hash] params 14 | def self.generate(params) 15 | data = {} 16 | data.merge!(params[:schema].schema_example(params[:link]["schema"])) 17 | 18 | result = [] 19 | data.sort_by { |k, _| k.to_s }.each do |key, values| 20 | [values].flatten.each do |value| 21 | result << [key.to_s, CGI.escape(value.to_s)].join("=") 22 | end 23 | end 24 | 25 | result 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/prmd/utils.rb: -------------------------------------------------------------------------------- 1 | module Prmd 2 | # For any tid bits, or core extension methods, without the "core" extension 3 | module Utils 4 | # For checking if the string contains only spaces 5 | BLANK_REGEX = /\A\s+\z/ 6 | 7 | def self.blank?(obj) 8 | if obj.nil? 9 | true 10 | elsif obj.is_a?(String) 11 | obj.empty? || !!(obj =~ BLANK_REGEX) 12 | elsif obj.respond_to?(:empty?) 13 | obj.empty? 14 | else 15 | false 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/prmd/version.rb: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | module Prmd 3 | # Well, duh, its a Version module, what did you expect? 4 | module Version 5 | MAJOR, MINOR, TEENY, PATCH = 0, 14, 0, nil 6 | # version string 7 | # @return [String] 8 | STRING = [MAJOR, MINOR, TEENY, PATCH].compact.join(".").freeze 9 | end 10 | # @return [String] 11 | VERSION = Version::STRING 12 | end 13 | -------------------------------------------------------------------------------- /prmd.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path("../lib", __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require "prmd/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "prmd" 7 | spec.version = Prmd::VERSION 8 | spec.authors = ["geemus"] 9 | spec.email = ["geemus@gmail.com"] 10 | spec.description = "scaffold, verify and generate docs from JSON Schema" 11 | spec.summary = "JSON Schema tooling" 12 | spec.homepage = "https://github.com/heroku/prmd" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files`.split($/) 16 | spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) } 17 | spec.require_paths = ["lib"] 18 | 19 | spec.required_ruby_version = ">= 3.2" 20 | 21 | spec.add_dependency "erubis", "~> 2.7" 22 | spec.add_dependency "json_schema", "~> 0.3", ">= 0.3.1" 23 | 24 | spec.add_development_dependency "bundler", "~> 2.0" 25 | spec.add_development_dependency "rake", ">= 12.3.3" 26 | spec.add_development_dependency "minitest", "~> 5.25" 27 | spec.add_development_dependency "rubocop", "~> 1.71" 28 | end 29 | -------------------------------------------------------------------------------- /schemas/hyper-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/hyper-schema#", 3 | "id": "http://json-schema.org/draft-04/hyper-schema#", 4 | "title": "JSON Hyper-Schema", 5 | "allOf": [ 6 | { 7 | "$ref": "http://json-schema.org/draft-04/schema#" 8 | } 9 | ], 10 | "properties": { 11 | "additionalItems": { 12 | "anyOf": [ 13 | { 14 | "type": "boolean" 15 | }, 16 | { 17 | "$ref": "#" 18 | } 19 | ] 20 | }, 21 | "additionalProperties": { 22 | "anyOf": [ 23 | { 24 | "type": "boolean" 25 | }, 26 | { 27 | "$ref": "#" 28 | } 29 | ] 30 | }, 31 | "dependencies": { 32 | "additionalProperties": { 33 | "anyOf": [ 34 | { 35 | "$ref": "#" 36 | }, 37 | { 38 | "type": "array" 39 | } 40 | ] 41 | } 42 | }, 43 | "items": { 44 | "anyOf": [ 45 | { 46 | "$ref": "#" 47 | }, 48 | { 49 | "$ref": "#/definitions/schemaArray" 50 | } 51 | ] 52 | }, 53 | "definitions": { 54 | "additionalProperties": { 55 | "$ref": "#" 56 | } 57 | }, 58 | "patternProperties": { 59 | "additionalProperties": { 60 | "$ref": "#" 61 | } 62 | }, 63 | "properties": { 64 | "additionalProperties": { 65 | "$ref": "#" 66 | } 67 | }, 68 | "allOf": { 69 | "$ref": "#/definitions/schemaArray" 70 | }, 71 | "anyOf": { 72 | "$ref": "#/definitions/schemaArray" 73 | }, 74 | "oneOf": { 75 | "$ref": "#/definitions/schemaArray" 76 | }, 77 | "not": { 78 | "$ref": "#" 79 | }, 80 | 81 | "links": { 82 | "type": "array", 83 | "items": { 84 | "$ref": "#/definitions/linkDescription" 85 | } 86 | }, 87 | "fragmentResolution": { 88 | "type": "string" 89 | }, 90 | "media": { 91 | "type": "object", 92 | "properties": { 93 | "type": { 94 | "description": "A media type, as described in RFC 2046", 95 | "type": "string" 96 | }, 97 | "binaryEncoding": { 98 | "description": "A content encoding scheme, as described in RFC 2045", 99 | "type": "string" 100 | } 101 | } 102 | }, 103 | "pathStart": { 104 | "description": "Instances' URIs must start with this value for this schema to apply to them", 105 | "type": "string", 106 | "format": "uri" 107 | } 108 | }, 109 | "definitions": { 110 | "schemaArray": { 111 | "type": "array", 112 | "items": { 113 | "$ref": "#" 114 | } 115 | }, 116 | "linkDescription": { 117 | "title": "Link Description Object", 118 | "type": "object", 119 | "required": [ "href", "rel" ], 120 | "properties": { 121 | "href": { 122 | "description": "a URI template, as defined by RFC 6570, with the addition of the $, ( and ) characters for pre-processing", 123 | "type": "string" 124 | }, 125 | "rel": { 126 | "description": "relation to the target resource of the link", 127 | "type": "string" 128 | }, 129 | "title": { 130 | "description": "a title for the link", 131 | "type": "string" 132 | }, 133 | "targetSchema": { 134 | "description": "JSON Schema describing the link target", 135 | "$ref": "#" 136 | }, 137 | "mediaType": { 138 | "description": "media type (as defined by RFC 2046) describing the link target", 139 | "type": "string" 140 | }, 141 | "method": { 142 | "description": "method for requesting the target of the link (e.g. for HTTP this might be \"GET\" or \"DELETE\")", 143 | "type": "string" 144 | }, 145 | "encType": { 146 | "description": "The media type in which to submit data along with the request", 147 | "type": "string", 148 | "default": "application/json" 149 | }, 150 | "schema": { 151 | "description": "Schema describing the data to submit along with the request", 152 | "$ref": "#" 153 | } 154 | } 155 | } 156 | }, 157 | "links": [ 158 | { 159 | "rel": "self", 160 | "href": "{+id}" 161 | }, 162 | { 163 | "rel": "full", 164 | "href": "{+($ref)}" 165 | } 166 | ] 167 | } 168 | 169 | -------------------------------------------------------------------------------- /schemas/interagent-hyper-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://interagent.github.io/interagent-hyper-schema#", 3 | "id": "http://interagent.github.io/interagent-hyper-schema#", 4 | "title": "Heroku JSON Hyper-Schema", 5 | "allOf": [ 6 | { 7 | "$ref": "http://json-schema.org/draft-04/hyper-schema#" 8 | } 9 | ], 10 | "definitions": { 11 | "identity": { 12 | "anyOf": [ 13 | { 14 | "$ref": "#/definitions/ref" 15 | }, 16 | { 17 | "additionalProperties": false, 18 | "properties": { 19 | "anyOf": { 20 | "additionalProperties": { 21 | "$ref": "#/definitions/ref" 22 | }, 23 | "minProperties": 1 24 | } 25 | }, 26 | "required": ["anyOf"] 27 | }, 28 | { 29 | "properties": {}, 30 | "strictProperties": true 31 | } 32 | ] 33 | }, 34 | "ref": { 35 | "additionalProperties": false, 36 | "properties": { 37 | "$ref": { 38 | "type": "string" 39 | } 40 | }, 41 | "required": ["$ref"] 42 | }, 43 | "resource": { 44 | "dependencies": { 45 | "properties": { 46 | "properties": { 47 | "definitions": { 48 | "additionalProperties": { 49 | "$ref": "#/definitions/resourceDefinition" 50 | }, 51 | "properties": { 52 | "identity": { 53 | "$ref": "#/definitions/identity" 54 | } 55 | }, 56 | "required": ["identity"] 57 | } 58 | } 59 | } 60 | }, 61 | "properties": { 62 | "links": { 63 | "items": { 64 | "$ref": "#/definitions/resourceLink" 65 | } 66 | }, 67 | "properties": { 68 | "patternProperties": { 69 | "^[a-z0-9][a-zA-Z0-9_]*[a-zA-Z0-9]$": { 70 | "$ref": "#/definitions/resourceProperty" 71 | } 72 | }, 73 | "strictProperties": true 74 | }, 75 | "strictProperties": { 76 | "enum": [true], 77 | "type": "boolean" 78 | } 79 | }, 80 | "required": [ 81 | "definitions", 82 | "description", 83 | "links", 84 | "title", 85 | "type" 86 | ] 87 | }, 88 | "resourceDefinition": { 89 | "anyOf": [ 90 | { 91 | "required": ["example", "type"] 92 | }, 93 | { 94 | "required": ["type"], 95 | "type": ["object"] 96 | } 97 | ], 98 | "not": { 99 | "required": ["links"] 100 | }, 101 | "required": ["description"] 102 | }, 103 | "resourceLink": { 104 | "properties": { 105 | "href": { 106 | "pattern": "^(\/((?!-|_)[a-z0-9_\\-]+(? "/apps/{(#/definitions/app)}", 13 | }) 14 | assert_equal "/apps/{(%23%2Fdefinitions%2Fapp)}", escaped_href 15 | end 16 | 17 | def test_resource_link_href_no_double_escaping 18 | pointer("#/definitions/app/links/0").merge!({ 19 | "href" => "/apps/{(%23%2Fdefinitions%2Fapp)}", 20 | }) 21 | assert_equal "/apps/{(%23%2Fdefinitions%2Fapp)}", escaped_href 22 | end 23 | 24 | def test_resource_link_href_no_side_effects 25 | pointer("#/definitions/app/links/0").merge!({ 26 | "href" => "/apps/foo#bar", 27 | }) 28 | assert_equal "/apps/foo#bar", escaped_href 29 | end 30 | 31 | private 32 | 33 | def data 34 | @data ||= { 35 | "$schema" => "http://interagent.github.io/interagent-hyper-schema", 36 | "description" => "My simple example API.", 37 | "id" => "http://example.com/schema", 38 | "title" => "Example API", 39 | "definitions" => { 40 | "app" => { 41 | "description" => "An app in our PaaS ecosystem.", 42 | "title" => "App", 43 | "type" => "object", 44 | "definitions" => {}, 45 | "links" => [ 46 | { 47 | "description" => "Create a new app.", 48 | "href" => "/apps", 49 | "method" => "POST", 50 | "rel" => "create", 51 | "title" => "Create App", 52 | }, 53 | ], 54 | "properties" => {}, 55 | }, 56 | }, 57 | "links" => [], 58 | "properties" => {}, 59 | "type" => "object", 60 | } 61 | end 62 | 63 | def pointer(path) 64 | JsonPointer.evaluate(data, path) 65 | end 66 | 67 | def escaped_href 68 | escaped = Prmd::Combine.__send__(:escape_hrefs, data) 69 | escaped["definitions"]["app"]["links"][0]["href"] 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /test/commands/init_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../helpers" 2 | 3 | class PrmdInitTest < Minitest::Test 4 | def test_init 5 | Prmd.init("Cake") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/commands/render_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../helpers" 2 | 3 | require "json_pointer" 4 | 5 | class InteragentRenderTest < Minitest::Test 6 | def test_render_for_valid_schema 7 | markdown = render 8 | 9 | assert_match(/An app in our PaaS ecosystem./, markdown) 10 | end 11 | 12 | def test_render_for_schema_with_property_defined_with_anyOf 13 | pointer("#/definitions/app").merge!({ 14 | "properties" => { 15 | "version" => { 16 | "anyOf" => [ 17 | { "type" => "string", "example" => "v10.9.rc1", "minLength" => 1 }, 18 | { "type" => "number", "minimum" => 0 }, 19 | ], 20 | }, 21 | }, 22 | }) 23 | markdown = render 24 | assert_match(/version.*v10\.9\.rc1/, markdown) 25 | end 26 | 27 | def test_render_for_schema_with_property_defined_with_oneOf 28 | markdown = render 29 | 30 | assert_match(/\*\*options\/\[OPTION1\]\.type\*\*/, markdown) 31 | assert_match(/\*\*options\/\[OPTION2\]\.type\*\*/, markdown) 32 | end 33 | 34 | def test_render_for_toc 35 | schema = Prmd::Schema.new(data) 36 | template = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "lib", "prmd", "templates")) 37 | markdown = Prmd.render(schema, template: template, doc: { toc: true }) 38 | 39 | assert_match(/^## The table of contents/, markdown) 40 | assert_match 'POST /apps', markdown 42 | assert_match '', markdown 43 | end 44 | 45 | def test_render_for_example_as_an_array 46 | # matches -d '[{...}]' taking into account line breaks and spacing 47 | expression = /-d '\[[\s\n]+\{[\n\s]+"name": "EXAMPLE",[\n\s]+"value": "example"[\s\n]+\}[\n\s]+\]/ 48 | markdown = render 49 | assert_match expression, markdown 50 | end 51 | 52 | def test_render_for_regex_patterns_with_pipes 53 | expression = /
\^[a-z\]\(\?:[a|b\]\)*[a-z]*\$/
 54 |     markdown = render
 55 |     assert_match expression, markdown
 56 |   end
 57 | 
 58 |   private
 59 | 
 60 |   def data
 61 |     @data ||= {
 62 |       "$schema" => "http://interagent.github.io/interagent-hyper-schema",
 63 |       "description" => "My simple example API.",
 64 |       "id" => "http://example.com/schema",
 65 |       "title" => "Example API",
 66 |       "definitions" => {
 67 |         "app" => {
 68 |           "description" => "An app in our PaaS ecosystem.",
 69 |           "title" => "App",
 70 |           "type" => "object",
 71 |           "definitions" => {
 72 |             "identity" => {
 73 |               "anyOf" => [
 74 |                 {
 75 |                   "$ref" => "#/definitions/app/definitions/name",
 76 |                 },
 77 |               ],
 78 |             },
 79 |             "name" => {
 80 |               "description" => "The app's name.",
 81 |               "type" => "string",
 82 |             },
 83 |           },
 84 |           "links" => [
 85 |             {
 86 |               "description" => "Create a new app.",
 87 |               "href" => "/apps",
 88 |               "method" => "POST",
 89 |               "rel" => "create",
 90 |               "title" => "Create App",
 91 |             },
 92 |           ],
 93 |           "properties" => {},
 94 |         },
 95 |         "config-var" => {
 96 |           "description" => "A configuration variable for an app.",
 97 |           "title" => "Config-var",
 98 |           "type" => "object",
 99 |           "definitions" => {
100 |             "name" => {
101 |               "description" => "The config-var's name.",
102 |               "type" => "string",
103 |               "example" => "EXAMPLE",
104 |             },
105 |             "value" => {
106 |               "description" => "The config-var's value.",
107 |               "type" => "string",
108 |               "example" => "example",
109 |             },
110 |             "option-type1" => {
111 |               "type" => "string",
112 |               "example" => "OPTION1",
113 |               "enum" => "OPTION1",
114 |             },
115 |             "option-type2" => {
116 |               "type" => "string",
117 |               "example" => "OPTION2",
118 |               "enum" => "OPTION2",
119 |             },
120 |             "patterned-string" => {
121 |               "description" => "A string with a regex pattern applied to it.",
122 |               "type" => "string",
123 |               "example" => "second",
124 |               "pattern" => "^[a-z](?:[a|b])*[a-z]*$",
125 |             },
126 |             "option1" => {
127 |               "properties" => {
128 |                 "type" => {
129 |                   "$ref" => "#/definitions/config-var/definitions/option-type1",
130 |                 },
131 |               },
132 |             },
133 |             "option2" => {
134 |               "properties" => {
135 |                 "type" => {
136 |                   "$ref" => "#/definitions/config-var/definitions/option-type2",
137 |                 },
138 |               },
139 |             },
140 |             "options" => {
141 |               "items" => {
142 |                 "example" => "CHOICE1",
143 |                 "oneOf" => [
144 |                   {
145 |                     "$ref" => "#/definitions/config-var/definitions/option1",
146 |                   },
147 |                   {
148 |                     "$ref" => "#/definitions/config-var/definitions/option2",
149 |                   },
150 |                 ],
151 |               },
152 |             },
153 |           },
154 |           "links" => [
155 |             {
156 |               "description" => "Create many config-vars.",
157 |               "href" => "/config-vars",
158 |               "method" => "PATCH",
159 |               "rel" => "instances",
160 |               "title" => "Create Config-var",
161 |               "schema" => {
162 |                 "type" => [
163 |                   "array",
164 |                 ],
165 |                 "items" => {
166 |                   "name" => {
167 |                     "$ref" => "#/definitions/config-var/definitions/name",
168 |                   },
169 |                   "value" => {
170 |                     "$ref" => "#/definitions/config-var/definitions/value",
171 |                   },
172 |                   "example" => [
173 |                     { "name" => "EXAMPLE", "value" => "example" },
174 |                   ],
175 |                 },
176 |               },
177 |             },
178 |           ],
179 |           "properties" => {
180 |             "name" => {
181 |               "$ref" => "#/definitions/config-var/definitions/name",
182 |             },
183 |             "value" => {
184 |               "$ref" => "#/definitions/config-var/definitions/value",
185 |             },
186 |             "options" => {
187 |               "$ref" => "#/definitions/config-var/definitions/options",
188 |             },
189 |             "patterned-string" => {
190 |               "$ref" => "#/definitions/config-var/definitions/patterned-string",
191 |             },
192 |           },
193 |         },
194 |       },
195 |       "links" => [
196 |         {
197 |           "href" => "https://example.com",
198 |           "rel" => "self",
199 |         },
200 |       ],
201 |       "properties" => {
202 |         "app" => {
203 |           "$ref" => "#/definitions/app",
204 |         },
205 |         "config-var" => {
206 |           "$ref" => "#/definitions/config-var",
207 |         },
208 |       },
209 |       "type" => "object",
210 |     }
211 |   end
212 | 
213 |   def pointer(path)
214 |     JsonPointer.evaluate(data, path)
215 |   end
216 | 
217 |   def render
218 |     schema = Prmd::Schema.new(data)
219 | 
220 |     template = File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "lib", "prmd", "templates"))
221 | 
222 |     Prmd.render(schema, template: template)
223 |   end
224 | end
225 | 


--------------------------------------------------------------------------------
/test/commands/verify_test.rb:
--------------------------------------------------------------------------------
  1 | require_relative "../helpers"
  2 | 
  3 | require "json_pointer"
  4 | 
  5 | class InteragentHyperSchemaVerifyTest < Minitest::Test
  6 |   def test_verifies
  7 |     assert_equal [], verify
  8 |   end
  9 | 
 10 |   #
 11 |   # api (root)
 12 |   #
 13 | 
 14 |   def test_api_required
 15 |     data.delete("title")
 16 |     errors = verify
 17 |     assert_equal 1, errors.count
 18 |     assert_match(/^#: /, errors[0])
 19 |     assert_match(/"title" wasn't supplied\./, errors[0])
 20 |   end
 21 | 
 22 |   def test_api_property_format
 23 |     pointer("#/properties").merge!({
 24 |       "app" => {},
 25 |     })
 26 |     errors = verify
 27 |     assert_match %r{^#/properties/app: }, errors[0]
 28 |     assert_match(/"\$ref" wasn't supplied\./, errors[0])
 29 |   end
 30 | 
 31 |   #
 32 |   # resource
 33 |   #
 34 | 
 35 |   def test_resource_required
 36 |     pointer("#/definitions/app").delete("title")
 37 |     errors = verify
 38 |     assert_equal 1, errors.count
 39 |     assert_match %r{^#/definitions/app: }, errors[0]
 40 |     assert_match(/"title" wasn't supplied\./, errors[0])
 41 |   end
 42 | 
 43 |   def test_resource_identity_format
 44 |     pointer("#/definitions/app/definitions/identity").merge!({
 45 |       "type" => "string",
 46 |     })
 47 |     errors = verify
 48 |     assert_equal 1, errors.count
 49 |     assert_match %r{^#/definitions/app/definitions/identity: }, errors[0]
 50 |     assert_match(/No subschema in "anyOf" matched\./, errors[0])
 51 |   end
 52 | 
 53 |   # an empty schema can be specified to bypass the identity check
 54 |   def test_resource_identity_format_empty
 55 |     pointer("#/definitions/app/definitions").merge!({
 56 |       "identity" => {},
 57 |     })
 58 |     assert_equal [], verify
 59 |   end
 60 | 
 61 |   # "my-property" does match fit our regex of lowercase letters and underscores only
 62 |   def test_resource_property_format
 63 |     pointer("#/definitions/app/properties").merge!({
 64 |       "my-property" => {},
 65 |     })
 66 |     errors = verify
 67 |     assert_equal 1, errors.count
 68 |     assert_match %r{^#/definitions/app/properties: }, errors[0]
 69 |     assert_match(/"my-property" is not a permitted key\./, errors[0])
 70 |   end
 71 | 
 72 |   def test_resource_strict_properties
 73 |     pointer("#/definitions/app").merge!({
 74 |       "strictProperties" => false,
 75 |     })
 76 |     errors = verify
 77 |     assert_equal 1, errors.count
 78 |     assert_match %r{^#/definitions/app/strictProperties: }, errors[0]
 79 |     assert_match(/false is not a member of \[true\]/, errors[0])
 80 |   end
 81 | 
 82 |   #
 83 |   # resource definition
 84 |   #
 85 | 
 86 |   def test_resource_definition_no_links
 87 |     pointer("#/definitions/app/definitions/name").merge!({
 88 |       "links" => [],
 89 |     })
 90 |     errors = verify
 91 |     assert_equal 1, errors.count
 92 |     assert_match %r{^#/definitions/app/definitions/name: }, errors[0]
 93 |     assert_match(/Matched "not" subschema/, errors[0])
 94 |   end
 95 | 
 96 |   def test_resource_definition_required
 97 |     pointer("#/definitions/app/definitions/name").delete("description")
 98 |     errors = verify
 99 |     assert_equal 1, errors.count
100 |     assert_match %r{^#/definitions/app/definitions/name: }, errors[0]
101 |     assert_match(/"description" wasn't supplied\./, errors[0])
102 |   end
103 | 
104 |   #
105 |   # resource link
106 |   #
107 | 
108 |   def test_resource_link_href_format
109 |     pointer("#/definitions/app/links/0").merge!({
110 |       "href" => "/my~apps",
111 |     })
112 |     errors = verify
113 |     assert_equal 1, errors.count
114 |     assert_match %r{^#/definitions/app/links/0/href: }, errors[0]
115 |     assert_match(/\/my~apps does not match /, errors[0])
116 |   end
117 | 
118 |   def test_resource_link_required
119 |     pointer("#/definitions/app/links/0").delete("method")
120 |     errors = verify
121 |     assert_equal 1, errors.count
122 |     assert_match %r{^#/definitions/app/links/0: }, errors[0]
123 |     assert_match(/"method" wasn't supplied\./, errors[0])
124 |   end
125 | 
126 |   private
127 | 
128 |   def data
129 |     @data ||= {
130 |       "$schema" => "http://interagent.github.io/interagent-hyper-schema",
131 |       "description" => "My simple example API.",
132 |       "id" => "http://example.com/schema",
133 |       "title" => "Example API",
134 |       "definitions" => {
135 |         "app" => {
136 |           "description" => "An app in our PaaS ecosystem.",
137 |           "title" => "App",
138 |           "type" => "object",
139 |           "definitions" => {
140 |             "identity" => {
141 |               "anyOf" => [
142 |                 {
143 |                   "$ref" => "#/definitions/app/definitions/name",
144 |                 },
145 |               ],
146 |             },
147 |             "name" => {
148 |               "description" => "The app's name.",
149 |               "type" => "string",
150 |             },
151 |           },
152 |           "links" => [
153 |             {
154 |               "description" => "Create a new app.",
155 |               "href" => "/apps",
156 |               "method" => "POST",
157 |               "rel" => "create",
158 |               "title" => "Create App",
159 |             },
160 |           ],
161 |           "properties" => {},
162 |         },
163 |       },
164 |       "links" => [
165 |         {
166 |           "href" => "https://example.com",
167 |           "rel" => "self",
168 |         },
169 |       ],
170 |       "properties" => {
171 |         "app" => {
172 |           "$ref" => "#/definitions/app",
173 |         },
174 |       },
175 |       "type" => "object",
176 |     }
177 |   end
178 | 
179 |   def pointer(path)
180 |     JsonPointer.evaluate(data, path)
181 |   end
182 | 
183 |   def verify
184 |     Prmd.verify(data)
185 |   end
186 | end
187 | 


--------------------------------------------------------------------------------
/test/core/reference_localizer_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative "../helpers"
 2 | 
 3 | module Prmd
 4 |   class ReferenceLocalizerTest < Minitest::Test
 5 |     def base_object
 6 |       {
 7 |         "type" => ["object"],
 8 |         "properties" => {
 9 |           "name" => {
10 |             "type" => ["nil", "string"],
11 |             "example" => "john",
12 |           },
13 |           "age" => {
14 |             "type" => ["nil", "number"],
15 |             "example" => 37,
16 |           },
17 |         },
18 |       }
19 |     end
20 | 
21 |     def test_no_references
22 |       object = base_object
23 |       assert_equal object, ReferenceLocalizer.localize(object)
24 |     end
25 | 
26 |     def test_simple_ref
27 |       object = base_object.merge(
28 |         "properties" => base_object["properties"].merge(
29 |           "name" => { "$ref" => "#/attributes/definitions/name" },
30 |         ),
31 |       )
32 | 
33 |       new_object = ReferenceLocalizer.localize(object)
34 |       assert_equal "#/definitions/attributes/definitions/name",
35 |         new_object["properties"]["name"]["$ref"]
36 |     end
37 | 
38 |     def test_simple_href
39 |       object = base_object.merge("href" => "%23%2Fschemata%2Fhello%2Fworld")
40 |       new_object = ReferenceLocalizer.localize(object)
41 |       assert_equal "%23%2Fdefinitions%2Fhello%2Fworld", new_object["href"]
42 | 
43 |       object = base_object.merge("href" => "%23%2Fhello%2Fworld")
44 |       new_object = ReferenceLocalizer.localize(object)
45 |       assert_equal "%2Fhello%2Fworld", new_object["href"]
46 |     end
47 | 
48 |     def test_aliases
49 |       object = base_object.merge(
50 |         "properties" => base_object["properties"].merge(
51 |           "name" => { "$ref" => "#/attributes/definitions/name" },
52 |         ),
53 |       )
54 |       object["properties"]["translated_name"] = object["properties"]["name"]
55 | 
56 |       new_object = ReferenceLocalizer.localize(object)
57 | 
58 |       assert_equal "#/definitions/attributes/definitions/name",
59 |         new_object["properties"]["name"]["$ref"]
60 | 
61 |       assert_equal "#/definitions/attributes/definitions/name",
62 |         new_object["properties"]["translated_name"]["$ref"]
63 |     end
64 |   end
65 | end
66 | 


--------------------------------------------------------------------------------
/test/helpers.rb:
--------------------------------------------------------------------------------
  1 | require "minitest"
  2 | require "minitest/autorun"
  3 | require "prmd"
  4 | require "prmd/cli/base"
  5 | 
  6 | module Prmd
  7 |   module CLI
  8 |     module Base
  9 |       # silence noop_execute
 10 |       def noop_execute(options = {})
 11 |         options
 12 |       end
 13 |     end
 14 |   end
 15 | end
 16 | 
 17 | module CliBaseTestHelpers
 18 |   def argv_for_test_run
 19 |     []
 20 |   end
 21 | 
 22 |   def options_for_test_run
 23 |     {}
 24 |   end
 25 | 
 26 |   def validate_parse_options(options)
 27 |   end
 28 | 
 29 |   def validate_run_options(options)
 30 |     assert_equal options[:noop], true
 31 |     validate_parse_options options
 32 |   end
 33 | 
 34 |   def command_module
 35 |   end
 36 | 
 37 |   def test_make_parser
 38 |     parser = command_module.make_parser
 39 |     assert_kind_of OptionParser, parser
 40 |   end
 41 | 
 42 |   def test_parse_options
 43 |     opts = command_module.parse_options(argv_for_test_run, options_for_test_run)
 44 | 
 45 |     validate_parse_options opts
 46 |   end
 47 | 
 48 |   def test_run
 49 |     opts = command_module.run(argv_for_test_run,
 50 |       options_for_test_run.merge(noop: true),)
 51 | 
 52 |     validate_run_options opts
 53 |   end
 54 | end
 55 | 
 56 | module PrmdTestHelpers
 57 |   module Paths
 58 |     def self.schemas(*)
 59 |       File.join(File.expand_path("schemata", File.dirname(__FILE__)), *)
 60 |     end
 61 | 
 62 |     def self.input_schemas(*)
 63 |       schemas("input", *)
 64 |     end
 65 | 
 66 |     def self.output_schemas(*)
 67 |       schemas("output", *)
 68 |     end
 69 |   end
 70 | end
 71 | 
 72 | def schemas_path(*)
 73 |   PrmdTestHelpers::Paths.schemas(*)
 74 | end
 75 | 
 76 | def input_schemas_path(*)
 77 |   PrmdTestHelpers::Paths.input_schemas(*)
 78 | end
 79 | 
 80 | def output_schemas_path(*)
 81 |   PrmdTestHelpers::Paths.output_schemas(*)
 82 | end
 83 | 
 84 | def user_input_schema
 85 |   @user_input_schema ||= Prmd.combine(input_schemas_path("user.json"))
 86 | end
 87 | 
 88 | module PrmdLinkTestHelpers
 89 |   def link_parent_required
 90 |     {
 91 |       "description" => "Create User",
 92 |       "href" => "/users",
 93 |       "method" => "POST",
 94 |       "rel" => "create",
 95 |       "schema" => {
 96 |         "properties" => {
 97 |           "user" => {
 98 |             "type" => ["object"], "properties" => { "email" => "string", "name" => "string" },
 99 |           },
100 |         },
101 |         "type" => ["object"],
102 |         "required" => ["user"],
103 |       },
104 |       "title" => "Create",
105 |     }
106 |   end
107 | 
108 |   def link_no_required
109 |     {
110 |       "description" => "Create User",
111 |       "href" => "/users",
112 |       "method" => "POST",
113 |       "rel" => "create",
114 |       "schema" => {
115 |         "properties" => {
116 |           "user" => {
117 |             "type" => ["object"], "properties" => { "email" => "string", "name" => "string" },
118 |           },
119 |         },
120 |         "type" => ["object"],
121 |       },
122 |       "title" => "Create",
123 |     }
124 |   end
125 | 
126 |   def link_child_required
127 |     {
128 |       "description" => "Create user",
129 |       "href" => "/users",
130 |       "method" => "POST",
131 |       "rel" => "create",
132 |       "schema" => {
133 |         "properties" => {
134 |           "user" => {
135 |             "type" => ["object"],
136 |             "properties" => {
137 |               "email" => "string",
138 |               "name" => "string",
139 |             },
140 |             "required" => ["email"],
141 |           },
142 |         },
143 |         "type" => ["object"],
144 |       },
145 |       "title" => "Create",
146 |     }
147 |   end
148 | 
149 |   def link_multiple_nested_required
150 |     {
151 |       "description" => "Create user",
152 |       "href" => "/users",
153 |       "method" => "POST",
154 |       "rel" => "create",
155 |       "schema" => {
156 |         "properties" => {
157 |           "user" => {
158 |             "type" => ["object"],
159 |             "properties" => {
160 |               "email" => "string",
161 |               "name" => "string",
162 |             },
163 |             "required" => ["email"],
164 |           },
165 |           "address" => {
166 |             "type" => ["object"],
167 |             "properties" => {
168 |               "street" => "string",
169 |               "zip" => "string",
170 |             },
171 |           },
172 |         },
173 |         "type" => ["object"],
174 |         "required" => ["address"],
175 |       },
176 |       "title" => "Create",
177 |     }
178 |   end
179 | end
180 | 


--------------------------------------------------------------------------------
/test/link_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative "helpers"
 2 | 
 3 | module Prmd
 4 |   class LinkTest < Minitest::Test
 5 |     include PrmdLinkTestHelpers
 6 | 
 7 |     [
 8 |       {
 9 |         title: "no_required",
10 |         required: {},
11 |         optional: { "user:email" => "string", "user:name" => "string" },
12 |       },
13 |       {
14 |         title: "parent_required",
15 |         optional: {},
16 |         required: { "user:email" => "string", "user:name" => "string" },
17 |       },
18 |       {
19 |         title: "child_required",
20 |         optional: { "user:name" => "string" },
21 |         required: { "user:email" => "string" },
22 |       },
23 |       {
24 |         title: "multiple_nested_required",
25 |         optional: { "user:name" => "string" },
26 |         required: { "user:email" => "string",
27 |                     "address:street" => "string",
28 |                     "address:zip" => "string", },
29 |       },
30 |     ].each do |test_hash|
31 |       define_method "test_#{test_hash[:title]}" do
32 |         subject = Prmd::Link.new(send("link_#{test_hash[:title]}"))
33 |         required, optional = subject.required_and_optional_parameters
34 | 
35 |         assert_equal required, test_hash[:required]
36 |         assert_equal optional, test_hash[:optional]
37 |       end
38 |     end
39 |   end
40 | end
41 | 


--------------------------------------------------------------------------------
/test/multi_loader/common.rb:
--------------------------------------------------------------------------------
 1 | require_relative "../helpers"
 2 | 
 3 | module PrmdLoaderTests
 4 |   # @abstrac
 5 |   def testing_filename
 6 |   end
 7 | 
 8 |   # @abstract
 9 |   def loader_module
10 |   end
11 | 
12 |   def assert_test_data(data)
13 |     assert_kind_of Hash, data
14 |     assert_equal "yes", data["test"]
15 |     assert_kind_of Hash, data["object"]
16 |     assert_equal "Object", data["object"]["is_a"]
17 |   end
18 | 
19 |   def test_load_data
20 |     data = File.read(testing_filename)
21 |     assert_test_data loader_module.load_data(data)
22 |   end
23 | 
24 |   def test_load_stream
25 |     File.open(testing_filename, "r") do |f|
26 |       assert_test_data loader_module.load_stream(f)
27 |     end
28 |   end
29 | 
30 |   def test_load_file
31 |     assert_test_data loader_module.load_file(testing_filename)
32 |   end
33 | end
34 | 


--------------------------------------------------------------------------------
/test/multi_loader/json_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative "common"
 2 | require "prmd/multi_loader/json"
 3 | 
 4 | class PrmdMultiLoaderJsonTest < Minitest::Test
 5 |   include PrmdLoaderTests
 6 | 
 7 |   def loader_module
 8 |     Prmd::MultiLoader::Json
 9 |   end
10 | 
11 |   def testing_filename
12 |     schemas_path("data/test.json")
13 |   end
14 | end
15 | 


--------------------------------------------------------------------------------
/test/multi_loader/toml_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative "common"
 2 | begin
 3 |   require "prmd/multi_loader/toml"
 4 | rescue LoadError
 5 | end
 6 | 
 7 | if defined?(TOML)
 8 |   class PrmdMultiLoaderTomlTest < Minitest::Test
 9 |     include PrmdLoaderTests
10 | 
11 |     def loader_module
12 |       Prmd::MultiLoader::Toml
13 |     end
14 | 
15 |     def testing_filename
16 |       schemas_path("data/test.toml")
17 |     end
18 |   end
19 | end
20 | 


--------------------------------------------------------------------------------
/test/multi_loader/yajl_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative "common"
 2 | begin
 3 |   require "prmd/multi_loader/yajl"
 4 | rescue LoadError
 5 | end
 6 | 
 7 | if defined?(Yajl)
 8 |   class PrmdMultiLoaderYajlTest < Minitest::Test
 9 |     include PrmdLoaderTests
10 | 
11 |     def loader_module
12 |       Prmd::MultiLoader::Yajl
13 |     end
14 | 
15 |     def testing_filename
16 |       schemas_path("data/test.json")
17 |     end
18 |   end
19 | end
20 | 


--------------------------------------------------------------------------------
/test/multi_loader/yaml_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative "common"
 2 | require "prmd/multi_loader/yaml"
 3 | 
 4 | class PrmdMultiLoaderYamlTest < Minitest::Test
 5 |   include PrmdLoaderTests
 6 | 
 7 |   def loader_module
 8 |     Prmd::MultiLoader::Yaml
 9 |   end
10 | 
11 |   def testing_filename
12 |     schemas_path("data/test.yaml")
13 |   end
14 | end
15 | 


--------------------------------------------------------------------------------
/test/rake_tasks/combine_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative "../helpers"
 2 | require "prmd/rake_tasks/combine"
 3 | require "rake"
 4 | 
 5 | # due to the nature of these Rake Tests, this should not be executed in a
 6 | # read-only filesystem or directory.
 7 | class PrmdRakeTaskCombineTest < Minitest::Test
 8 |   def test_define_wo_options
 9 |     paths = [input_schemas_path("rake_combine")]
10 |     # output_file = output_schemas_path('rake_combine_with_options.json')
11 |     output_file = nil
12 |     if output_file
13 |       File.delete(output_file) if File.exist?(output_file)
14 |     end
15 |     Prmd::RakeTasks::Combine.new do |t|
16 |       t.name = :combine_wo_options
17 |       t.options[:meta] = input_schemas_path("rake-meta.json")
18 |       t.paths.concat(paths)
19 |       t.output_file = output_file
20 |     end
21 |     Rake::Task["combine_wo_options"].invoke
22 |     assert File.exist?(output_file) if output_file
23 |   end
24 | 
25 |   def test_define_with_options
26 |     paths = [input_schemas_path("rake_combine")]
27 |     # output_file = output_schemas_path('rake_combine_with_options.json')
28 |     output_file = nil
29 |     options = {
30 |       meta: input_schemas_path("rake-meta.json"),
31 |     }
32 |     if output_file
33 |       File.delete(output_file) if File.exist?(output_file)
34 |     end
35 |     Prmd::RakeTasks::Combine.new(name: :combine_with_options,
36 |       paths: paths,
37 |       output_file: output_file,
38 |       options: options,)
39 |     Rake::Task["combine_with_options"].invoke
40 |     assert File.exist?(output_file) if output_file
41 |   end
42 | end
43 | 


--------------------------------------------------------------------------------
/test/rake_tasks/doc_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative "../helpers"
 2 | require "prmd/rake_tasks/doc"
 3 | require "rake"
 4 | 
 5 | # due to the nature of these Rake Tests, this should not be executed in a
 6 | # read-only filesystem or directory.
 7 | class PrmdRakeTaskDocTest < Minitest::Test
 8 |   def test_define_wo_options
 9 |     input_file = input_schemas_path("rake_doc.json")
10 |     # output_file = output_schemas_path('rake_doc_with_options.md')
11 |     output_file = nil
12 |     if output_file
13 |       File.delete(output_file) if File.exist?(output_file)
14 |     end
15 |     Prmd::RakeTasks::Doc.new do |t|
16 |       t.name = :doc_wo_options
17 |       t.files = { input_file => output_file }
18 |     end
19 |     Rake::Task["doc_wo_options"].invoke
20 |     assert File.exist?(output_file) if output_file
21 |   end
22 | 
23 |   def test_define_with_options
24 |     input_file = input_schemas_path("rake_doc.json")
25 |     # output_file = output_schemas_path('rake_doc_with_options.md')
26 |     output_file = nil
27 |     if output_file
28 |       File.delete(output_file) if File.exist?(output_file)
29 |     end
30 |     Prmd::RakeTasks::Doc.new(name: :doc_with_options,
31 |       files: { input_file => output_file },)
32 |     Rake::Task["doc_with_options"].invoke
33 |     assert File.exist?(output_file) if output_file
34 |   end
35 | end
36 | 


--------------------------------------------------------------------------------
/test/rake_tasks/verify_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative "../helpers"
 2 | require "prmd/rake_tasks/verify"
 3 | require "rake"
 4 | 
 5 | # due to the nature of these Rake Tests, this should not be executed in a
 6 | # read-only filesystem or directory.
 7 | class PrmdRakeTaskVerifyTest < Minitest::Test
 8 |   def test_define_wo_options
 9 |     input_file = input_schemas_path("rake_verify.json")
10 |     Prmd::RakeTasks::Verify.new do |t|
11 |       t.name = :verify_wo_options
12 |       t.files = [input_file]
13 |     end
14 |     Rake::Task["verify_wo_options"].invoke
15 |   end
16 | 
17 |   def test_define_with_options
18 |     input_file = input_schemas_path("rake_verify.json")
19 |     Prmd::RakeTasks::Verify.new(name: :verify_with_options,
20 |       files: [input_file],)
21 |     Rake::Task["verify_with_options"].invoke
22 |   end
23 | end
24 | 


--------------------------------------------------------------------------------
/test/schema_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative "helpers"
 2 | 
 3 | class SchemaTest < Minitest::Test
 4 |   def test_dereference_with_ref
 5 |     key, value = user_input_schema.dereference(
 6 |       "$ref" => "#/definitions/user/definitions/id",
 7 |     )
 8 |     assert_equal(key,   "#/definitions/user/definitions/id")
 9 |     user_id = user_input_schema["definitions"]["user"]["definitions"]["id"]
10 |     assert_equal(value, user_id)
11 |   end
12 | 
13 |   def test_dereference_without_ref
14 |     key, value = user_input_schema.dereference(
15 |       "#/definitions/user/definitions/id",
16 |     )
17 |     assert_equal(key,   "#/definitions/user/definitions/id")
18 |     user_id = user_input_schema["definitions"]["user"]["definitions"]["id"]
19 |     assert_equal(value, user_id)
20 |   end
21 | 
22 |   def test_dereference_with_nested_ref
23 |     key, value = user_input_schema.dereference(
24 |       "$ref" => "#/definitions/user/definitions/identity",
25 |     )
26 |     assert_equal(key,   "#/definitions/user/definitions/id")
27 |     user_id = user_input_schema["definitions"]["user"]["definitions"]["id"]
28 |     assert_equal(value, user_id)
29 |   end
30 | 
31 |   def test_dereference_with_local_context
32 |     key, value = user_input_schema.dereference(
33 |       "$ref" => "#/definitions/user/properties/id",
34 |       "override" => true,
35 |     )
36 |     assert_equal(key,   "#/definitions/user/definitions/id")
37 |     user_id = user_input_schema["definitions"]["user"]["definitions"]["id"]
38 |     assert_equal(value, { "override" => true }.merge(user_id))
39 |   end
40 | end
41 | 


--------------------------------------------------------------------------------
/test/schemata/data/test.json:
--------------------------------------------------------------------------------
1 | {
2 |   "test": "yes",
3 |   "object": {
4 |     "is_a": "Object"
5 |   }
6 | }
7 | 


--------------------------------------------------------------------------------
/test/schemata/data/test.toml:
--------------------------------------------------------------------------------
1 | test = "yes"
2 | 
3 | [object]
4 | is_a = "Object"
5 | 


--------------------------------------------------------------------------------
/test/schemata/data/test.yaml:
--------------------------------------------------------------------------------
1 | test: "yes"
2 | object:
3 |   is_a: "Object"
4 | 


--------------------------------------------------------------------------------
/test/schemata/input/doc-settings.json:
--------------------------------------------------------------------------------
1 | {
2 |   // don't ask, its just for testing
3 |   "content_type": "application/bread"
4 | }
5 | 


--------------------------------------------------------------------------------
/test/schemata/input/meta.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "$schema": "http://json-schema.org/draft-04/hyper-schema",
 3 |   "definitions": {},
 4 |   "description": "API lets you interact with service",
 5 |   "id": "schemata",
 6 |   "links": [{
 7 |     "href": "https://api.example.com",
 8 |     "rel": "self"
 9 |   }],
10 |   "properties": {},
11 |   "title": "API",
12 |   "type": [
13 |     "object"
14 |   ]
15 | }
16 | 


--------------------------------------------------------------------------------
/test/schemata/input/rake-meta.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "id": "rake_task_meta",
 3 |   "title": "Rake Task Test",
 4 |   "links": [{
 5 |     "href": "https://prmd.rake_task_test.io",
 6 |     "rel": "self"
 7 |   }],
 8 |   "description": "Testing schema for Prmd RakeTasks"
 9 | }
10 | 


--------------------------------------------------------------------------------
/test/schemata/input/rake_combine/post.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "$schema": "http://json-schema.org/draft-04/hyper-schema",
  3 |   "title": "Post",
  4 |   "definitions": {
  5 |     "id": {
  6 |       "description": "unique identifier of post",
  7 |       "example": "01234567-89ab-cdef-0123-456789abcdef",
  8 |       "format": "uuid",
  9 |       "type": [
 10 |         "string"
 11 |       ]
 12 |     },
 13 |     "identity": {
 14 |       "$ref": "/schemata/post#/definitions/id"
 15 |     },
 16 |     "created_at": {
 17 |       "description": "when post was created",
 18 |       "example": "2012-01-01T12:00:00Z",
 19 |       "format": "date-time",
 20 |       "type": [
 21 |         "string"
 22 |       ]
 23 |     },
 24 |     "updated_at": {
 25 |       "description": "when post was updated",
 26 |       "example": "2012-01-01T12:00:00Z",
 27 |       "format": "date-time",
 28 |       "type": [
 29 |         "string"
 30 |       ]
 31 |     }
 32 |   },
 33 |   "description": "FIXME",
 34 |   "links": [
 35 |     {
 36 |       "description": "Create a new post.",
 37 |       "href": "/posts",
 38 |       "method": "POST",
 39 |       "rel": "create",
 40 |       "schema": {
 41 |         "properties": {
 42 |         },
 43 |         "type": [
 44 |           "object"
 45 |         ]
 46 |       },
 47 |       "title": "Create"
 48 |     },
 49 |     {
 50 |       "description": "Delete an existing post.",
 51 |       "href": "/posts/{(%2Fschemata%2Fpost%23%2Fdefinitions%2Fidentity)}",
 52 |       "method": "DELETE",
 53 |       "rel": "destroy",
 54 |       "title": "Delete"
 55 |     },
 56 |     {
 57 |       "description": "Info for existing post.",
 58 |       "href": "/posts/{(%2Fschemata%2Fpost%23%2Fdefinitions%2Fidentity)}",
 59 |       "method": "GET",
 60 |       "rel": "self",
 61 |       "title": "Info"
 62 |     },
 63 |     {
 64 |       "description": "List existing posts.",
 65 |       "href": "/posts",
 66 |       "method": "GET",
 67 |       "rel": "instances",
 68 |       "title": "List"
 69 |     },
 70 |     {
 71 |       "description": "Update an existing post.",
 72 |       "href": "/posts/{(%2Fschemata%2Fpost%23%2Fdefinitions%2Fidentity)}",
 73 |       "method": "PATCH",
 74 |       "rel": "update",
 75 |       "schema": {
 76 |         "properties": {
 77 |         },
 78 |         "type": [
 79 |           "object"
 80 |         ]
 81 |       },
 82 |       "title": "Update"
 83 |     }
 84 |   ],
 85 |   "properties": {
 86 |     "created_at": {
 87 |       "$ref": "/schemata/post#/definitions/created_at"
 88 |     },
 89 |     "id": {
 90 |       "$ref": "/schemata/post#/definitions/id"
 91 |     },
 92 |     "updated_at": {
 93 |       "$ref": "/schemata/post#/definitions/updated_at"
 94 |     }
 95 |   },
 96 |   "type": [
 97 |     "object"
 98 |   ],
 99 |   "id": "schemata/post"
100 | }
101 | 


--------------------------------------------------------------------------------
/test/schemata/input/rake_combine/user.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "$schema": "http://json-schema.org/draft-04/hyper-schema",
  3 |   "title": "User",
  4 |   "definitions": {
  5 |     "id": {
  6 |       "description": "unique identifier of user",
  7 |       "example": "01234567-89ab-cdef-0123-456789abcdef",
  8 |       "format": "uuid",
  9 |       "type": [
 10 |         "string"
 11 |       ]
 12 |     },
 13 |     "identity": {
 14 |       "$ref": "/schemata/user#/definitions/id"
 15 |     },
 16 |     "created_at": {
 17 |       "description": "when user was created",
 18 |       "example": "2012-01-01T12:00:00Z",
 19 |       "format": "date-time",
 20 |       "type": [
 21 |         "string"
 22 |       ]
 23 |     },
 24 |     "updated_at": {
 25 |       "description": "when user was updated",
 26 |       "example": "2012-01-01T12:00:00Z",
 27 |       "format": "date-time",
 28 |       "type": [
 29 |         "string"
 30 |       ]
 31 |     }
 32 |   },
 33 |   "description": "FIXME",
 34 |   "links": [
 35 |     {
 36 |       "description": "Create a new user.",
 37 |       "href": "/users",
 38 |       "method": "POST",
 39 |       "rel": "create",
 40 |       "schema": {
 41 |         "properties": {
 42 |         },
 43 |         "type": [
 44 |           "object"
 45 |         ]
 46 |       },
 47 |       "title": "Create"
 48 |     },
 49 |     {
 50 |       "description": "Delete an existing user.",
 51 |       "href": "/users/{(%2Fschemata%2Fuser%23%2Fdefinitions%2Fidentity)}",
 52 |       "method": "DELETE",
 53 |       "rel": "destroy",
 54 |       "title": "Delete"
 55 |     },
 56 |     {
 57 |       "description": "Info for existing user.",
 58 |       "href": "/users/{(%2Fschemata%2Fuser%23%2Fdefinitions%2Fidentity)}",
 59 |       "method": "GET",
 60 |       "rel": "self",
 61 |       "title": "Info"
 62 |     },
 63 |     {
 64 |       "description": "List existing users.",
 65 |       "href": "/users",
 66 |       "method": "GET",
 67 |       "rel": "instances",
 68 |       "title": "List"
 69 |     },
 70 |     {
 71 |       "description": "Update an existing user.",
 72 |       "href": "/users/{(%2Fschemata%2Fuser%23%2Fdefinitions%2Fidentity)}",
 73 |       "method": "PATCH",
 74 |       "rel": "update",
 75 |       "schema": {
 76 |         "properties": {
 77 |         },
 78 |         "type": [
 79 |           "object"
 80 |         ]
 81 |       },
 82 |       "title": "Update"
 83 |     }
 84 |   ],
 85 |   "properties": {
 86 |     "created_at": {
 87 |       "$ref": "/schemata/user#/definitions/created_at"
 88 |     },
 89 |     "id": {
 90 |       "$ref": "/schemata/user#/definitions/id"
 91 |     },
 92 |     "updated_at": {
 93 |       "$ref": "/schemata/user#/definitions/updated_at"
 94 |     }
 95 |   },
 96 |   "type": [
 97 |     "object"
 98 |   ],
 99 |   "id": "schemata/user"
100 | }
101 | 


--------------------------------------------------------------------------------
/test/schemata/input/rake_doc.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "$schema": "http://json-schema.org/draft-04/hyper-schema",
  3 |   "definitions": {
  4 |     "post": {
  5 |       "$schema": "http://json-schema.org/draft-04/hyper-schema",
  6 |       "title": "Post",
  7 |       "definitions": {
  8 |         "id": {
  9 |           "description": "unique identifier of post",
 10 |           "example": "01234567-89ab-cdef-0123-456789abcdef",
 11 |           "format": "uuid",
 12 |           "type": [
 13 |             "string"
 14 |           ]
 15 |         },
 16 |         "identity": {
 17 |           "$ref": "#/definitions/post/definitions/id"
 18 |         },
 19 |         "created_at": {
 20 |           "description": "when post was created",
 21 |           "example": "2012-01-01T12:00:00Z",
 22 |           "format": "date-time",
 23 |           "type": [
 24 |             "string"
 25 |           ]
 26 |         },
 27 |         "updated_at": {
 28 |           "description": "when post was updated",
 29 |           "example": "2012-01-01T12:00:00Z",
 30 |           "format": "date-time",
 31 |           "type": [
 32 |             "string"
 33 |           ]
 34 |         }
 35 |       },
 36 |       "description": "FIXME",
 37 |       "links": [
 38 |         {
 39 |           "description": "Create a new post.",
 40 |           "href": "/posts",
 41 |           "method": "POST",
 42 |           "rel": "create",
 43 |           "schema": {
 44 |             "properties": {
 45 |             },
 46 |             "type": [
 47 |               "object"
 48 |             ]
 49 |           },
 50 |           "title": "Create"
 51 |         },
 52 |         {
 53 |           "description": "Delete an existing post.",
 54 |           "href": "/posts/{(%23%2Fdefinitions%2Fpost%2Fdefinitions%2Fidentity)}",
 55 |           "method": "DELETE",
 56 |           "rel": "destroy",
 57 |           "title": "Delete"
 58 |         },
 59 |         {
 60 |           "description": "Info for existing post.",
 61 |           "href": "/posts/{(%23%2Fdefinitions%2Fpost%2Fdefinitions%2Fidentity)}",
 62 |           "method": "GET",
 63 |           "rel": "self",
 64 |           "title": "Info"
 65 |         },
 66 |         {
 67 |           "description": "List existing posts.",
 68 |           "href": "/posts",
 69 |           "method": "GET",
 70 |           "rel": "instances",
 71 |           "title": "List"
 72 |         },
 73 |         {
 74 |           "description": "Update an existing post.",
 75 |           "href": "/posts/{(%23%2Fdefinitions%2Fpost%2Fdefinitions%2Fidentity)}",
 76 |           "method": "PATCH",
 77 |           "rel": "update",
 78 |           "schema": {
 79 |             "properties": {
 80 |             },
 81 |             "type": [
 82 |               "object"
 83 |             ]
 84 |           },
 85 |           "title": "Update"
 86 |         }
 87 |       ],
 88 |       "properties": {
 89 |         "created_at": {
 90 |           "$ref": "#/definitions/post/definitions/created_at"
 91 |         },
 92 |         "id": {
 93 |           "$ref": "#/definitions/post/definitions/id"
 94 |         },
 95 |         "updated_at": {
 96 |           "$ref": "#/definitions/post/definitions/updated_at"
 97 |         }
 98 |       },
 99 |       "type": [
100 |         "object"
101 |       ]
102 |     },
103 |     "user": {
104 |       "$schema": "http://json-schema.org/draft-04/hyper-schema",
105 |       "title": "User",
106 |       "definitions": {
107 |         "id": {
108 |           "description": "unique identifier of user",
109 |           "example": "01234567-89ab-cdef-0123-456789abcdef",
110 |           "format": "uuid",
111 |           "type": [
112 |             "string"
113 |           ]
114 |         },
115 |         "identity": {
116 |           "$ref": "#/definitions/user/definitions/id"
117 |         },
118 |         "created_at": {
119 |           "description": "when user was created",
120 |           "example": "2012-01-01T12:00:00Z",
121 |           "format": "date-time",
122 |           "type": [
123 |             "string"
124 |           ]
125 |         },
126 |         "updated_at": {
127 |           "description": "when user was updated",
128 |           "example": "2012-01-01T12:00:00Z",
129 |           "format": "date-time",
130 |           "type": [
131 |             "string"
132 |           ]
133 |         }
134 |       },
135 |       "description": "FIXME",
136 |       "links": [
137 |         {
138 |           "description": "Create a new user.",
139 |           "href": "/users",
140 |           "method": "POST",
141 |           "rel": "create",
142 |           "schema": {
143 |             "properties": {
144 |             },
145 |             "type": [
146 |               "object"
147 |             ]
148 |           },
149 |           "title": "Create"
150 |         },
151 |         {
152 |           "description": "Delete an existing user.",
153 |           "href": "/users/{(%23%2Fdefinitions%2Fuser%2Fdefinitions%2Fidentity)}",
154 |           "method": "DELETE",
155 |           "rel": "destroy",
156 |           "title": "Delete"
157 |         },
158 |         {
159 |           "description": "Info for existing user.",
160 |           "href": "/users/{(%23%2Fdefinitions%2Fuser%2Fdefinitions%2Fidentity)}",
161 |           "method": "GET",
162 |           "rel": "self",
163 |           "title": "Info"
164 |         },
165 |         {
166 |           "description": "List existing users.",
167 |           "href": "/users",
168 |           "method": "GET",
169 |           "rel": "instances",
170 |           "title": "List"
171 |         },
172 |         {
173 |           "description": "Update an existing user.",
174 |           "href": "/users/{(%23%2Fdefinitions%2Fuser%2Fdefinitions%2Fidentity)}",
175 |           "method": "PATCH",
176 |           "rel": "update",
177 |           "schema": {
178 |             "properties": {
179 |             },
180 |             "type": [
181 |               "object"
182 |             ]
183 |           },
184 |           "title": "Update"
185 |         }
186 |       ],
187 |       "properties": {
188 |         "created_at": {
189 |           "$ref": "#/definitions/user/definitions/created_at"
190 |         },
191 |         "id": {
192 |           "$ref": "#/definitions/user/definitions/id"
193 |         },
194 |         "updated_at": {
195 |           "$ref": "#/definitions/user/definitions/updated_at"
196 |         }
197 |       },
198 |       "type": [
199 |         "object"
200 |       ]
201 |     }
202 |   },
203 |   "properties": {
204 |     "post": {
205 |       "$ref": "#/definitions/post"
206 |     },
207 |     "user": {
208 |       "$ref": "#/definitions/user"
209 |     }
210 |   },
211 |   "type": [
212 |     "object"
213 |   ],
214 |   "id": "rake_task_meta",
215 |   "title": "Rake Task Test",
216 |   "links": [
217 |     {
218 |       "href": "https://prmd.rake_task_test.io",
219 |       "rel": "self"
220 |     }
221 |   ],
222 |   "description": "Testing schema for Prmd RakeTasks"
223 | }
224 | 


--------------------------------------------------------------------------------
/test/schemata/input/rake_verify.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "$schema": "http://json-schema.org/draft-04/hyper-schema",
  3 |   "definitions": {
  4 |     "post": {
  5 |       "$schema": "http://json-schema.org/draft-04/hyper-schema",
  6 |       "title": "Post",
  7 |       "definitions": {
  8 |         "id": {
  9 |           "description": "unique identifier of post",
 10 |           "example": "01234567-89ab-cdef-0123-456789abcdef",
 11 |           "format": "uuid",
 12 |           "type": [
 13 |             "string"
 14 |           ]
 15 |         },
 16 |         "identity": {
 17 |           "$ref": "#/definitions/post/definitions/id"
 18 |         },
 19 |         "created_at": {
 20 |           "description": "when post was created",
 21 |           "example": "2012-01-01T12:00:00Z",
 22 |           "format": "date-time",
 23 |           "type": [
 24 |             "string"
 25 |           ]
 26 |         },
 27 |         "updated_at": {
 28 |           "description": "when post was updated",
 29 |           "example": "2012-01-01T12:00:00Z",
 30 |           "format": "date-time",
 31 |           "type": [
 32 |             "string"
 33 |           ]
 34 |         }
 35 |       },
 36 |       "description": "FIXME",
 37 |       "links": [
 38 |         {
 39 |           "description": "Create a new post.",
 40 |           "href": "/posts",
 41 |           "method": "POST",
 42 |           "rel": "create",
 43 |           "schema": {
 44 |             "properties": {
 45 |             },
 46 |             "type": [
 47 |               "object"
 48 |             ]
 49 |           },
 50 |           "title": "Create"
 51 |         },
 52 |         {
 53 |           "description": "Delete an existing post.",
 54 |           "href": "/posts/{(%23%2Fdefinitions%2Fpost%2Fdefinitions%2Fidentity)}",
 55 |           "method": "DELETE",
 56 |           "rel": "destroy",
 57 |           "title": "Delete"
 58 |         },
 59 |         {
 60 |           "description": "Info for existing post.",
 61 |           "href": "/posts/{(%23%2Fdefinitions%2Fpost%2Fdefinitions%2Fidentity)}",
 62 |           "method": "GET",
 63 |           "rel": "self",
 64 |           "title": "Info"
 65 |         },
 66 |         {
 67 |           "description": "List existing posts.",
 68 |           "href": "/posts",
 69 |           "method": "GET",
 70 |           "rel": "instances",
 71 |           "title": "List"
 72 |         },
 73 |         {
 74 |           "description": "Update an existing post.",
 75 |           "href": "/posts/{(%23%2Fdefinitions%2Fpost%2Fdefinitions%2Fidentity)}",
 76 |           "method": "PATCH",
 77 |           "rel": "update",
 78 |           "schema": {
 79 |             "properties": {
 80 |             },
 81 |             "type": [
 82 |               "object"
 83 |             ]
 84 |           },
 85 |           "title": "Update"
 86 |         }
 87 |       ],
 88 |       "properties": {
 89 |         "created_at": {
 90 |           "$ref": "#/definitions/post/definitions/created_at"
 91 |         },
 92 |         "id": {
 93 |           "$ref": "#/definitions/post/definitions/id"
 94 |         },
 95 |         "updated_at": {
 96 |           "$ref": "#/definitions/post/definitions/updated_at"
 97 |         }
 98 |       },
 99 |       "type": [
100 |         "object"
101 |       ]
102 |     },
103 |     "user": {
104 |       "$schema": "http://json-schema.org/draft-04/hyper-schema",
105 |       "title": "User",
106 |       "definitions": {
107 |         "id": {
108 |           "description": "unique identifier of user",
109 |           "example": "01234567-89ab-cdef-0123-456789abcdef",
110 |           "format": "uuid",
111 |           "type": [
112 |             "string"
113 |           ]
114 |         },
115 |         "identity": {
116 |           "$ref": "#/definitions/user/definitions/id"
117 |         },
118 |         "created_at": {
119 |           "description": "when user was created",
120 |           "example": "2012-01-01T12:00:00Z",
121 |           "format": "date-time",
122 |           "type": [
123 |             "string"
124 |           ]
125 |         },
126 |         "updated_at": {
127 |           "description": "when user was updated",
128 |           "example": "2012-01-01T12:00:00Z",
129 |           "format": "date-time",
130 |           "type": [
131 |             "string"
132 |           ]
133 |         }
134 |       },
135 |       "description": "FIXME",
136 |       "links": [
137 |         {
138 |           "description": "Create a new user.",
139 |           "href": "/users",
140 |           "method": "POST",
141 |           "rel": "create",
142 |           "schema": {
143 |             "properties": {
144 |             },
145 |             "type": [
146 |               "object"
147 |             ]
148 |           },
149 |           "title": "Create"
150 |         },
151 |         {
152 |           "description": "Delete an existing user.",
153 |           "href": "/users/{(%23%2Fdefinitions%2Fuser%2Fdefinitions%2Fidentity)}",
154 |           "method": "DELETE",
155 |           "rel": "destroy",
156 |           "title": "Delete"
157 |         },
158 |         {
159 |           "description": "Info for existing user.",
160 |           "href": "/users/{(%23%2Fdefinitions%2Fuser%2Fdefinitions%2Fidentity)}",
161 |           "method": "GET",
162 |           "rel": "self",
163 |           "title": "Info"
164 |         },
165 |         {
166 |           "description": "List existing users.",
167 |           "href": "/users",
168 |           "method": "GET",
169 |           "rel": "instances",
170 |           "title": "List"
171 |         },
172 |         {
173 |           "description": "Update an existing user.",
174 |           "href": "/users/{(%23%2Fdefinitions%2Fuser%2Fdefinitions%2Fidentity)}",
175 |           "method": "PATCH",
176 |           "rel": "update",
177 |           "schema": {
178 |             "properties": {
179 |             },
180 |             "type": [
181 |               "object"
182 |             ]
183 |           },
184 |           "title": "Update"
185 |         }
186 |       ],
187 |       "properties": {
188 |         "created_at": {
189 |           "$ref": "#/definitions/user/definitions/created_at"
190 |         },
191 |         "id": {
192 |           "$ref": "#/definitions/user/definitions/id"
193 |         },
194 |         "updated_at": {
195 |           "$ref": "#/definitions/user/definitions/updated_at"
196 |         }
197 |       },
198 |       "type": [
199 |         "object"
200 |       ]
201 |     }
202 |   },
203 |   "properties": {
204 |     "post": {
205 |       "$ref": "#/definitions/post"
206 |     },
207 |     "user": {
208 |       "$ref": "#/definitions/user"
209 |     }
210 |   },
211 |   "type": [
212 |     "object"
213 |   ],
214 |   "id": "rake_task_meta",
215 |   "title": "Rake Task Test",
216 |   "links": [
217 |     {
218 |       "href": "https://prmd.rake_task_test.io",
219 |       "rel": "self"
220 |     }
221 |   ],
222 |   "description": "Testing schema for Prmd RakeTasks"
223 | }
224 | 


--------------------------------------------------------------------------------
/test/schemata/input/user.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "$schema": "http://json-schema.org/draft-04/hyper-schema",
  3 |   "title": "API - User",
  4 |   "type": [
  5 |     "object"
  6 |   ],
  7 |   "description": "User represents an API account.",
  8 |   "definitions": {
  9 |     "created_at": {
 10 |       "description": "when user was created",
 11 |       "example": "2012-01-01T12:00:00Z",
 12 |       "format": "date-time",
 13 |       "type": [
 14 |         "string"
 15 |       ]
 16 |     },
 17 |     "id": {
 18 |       "description": "unique identifier of user",
 19 |       "example": "01234567-89ab-cdef-0123-456789abcdef",
 20 |       "format": "uuid",
 21 |       "type": [
 22 |         "string"
 23 |       ]
 24 |     },
 25 |     "identity": {
 26 |       "$ref": "/schemata/user#/definitions/id"
 27 |     },
 28 |     "updated_at": {
 29 |       "description": "when user was updated",
 30 |       "example": "2012-01-01T12:00:00Z",
 31 |       "format": "date-time",
 32 |       "type": [
 33 |         "string"
 34 |       ]
 35 |     }
 36 |   },
 37 |   "links": [
 38 |     {
 39 |       "description": "Create a new user.",
 40 |       "href": "/users",
 41 |       "method": "POST",
 42 |       "rel": "create",
 43 |       "schema": {
 44 |         "properties": {
 45 |           "user": {
 46 |              "type": "object",
 47 |              "properties" : {
 48 |                 "email": "string",
 49 |                 "name": "string"
 50 |              },
 51 |              "required": ["email"]
 52 |           }
 53 |         },
 54 |         "type": [
 55 |           "object"
 56 |         ],
 57 |         "required": ["user"]
 58 |       },
 59 |       "title": "Create"
 60 |     },
 61 |     {
 62 |       "description": "Delete an existing user.",
 63 |       "href": "/users/{(%2Fschemata%2Fuser%23%2Fdefinitions%2Fidentity)}",
 64 |       "method": "DELETE",
 65 |       "rel": "destroy",
 66 |       "title": "Delete"
 67 |     },
 68 |     {
 69 |       "description": "Info for existing user.",
 70 |       "href": "/users/{(%2Fschemata%2Fuser%23%2Fdefinitions%2Fidentity)}",
 71 |       "method": "GET",
 72 |       "rel": "self",
 73 |       "title": "Info"
 74 |     },
 75 |     {
 76 |       "description": "List existing user.",
 77 |       "href": "/users",
 78 |       "method": "GET",
 79 |       "rel": "instances",
 80 |       "title": "List"
 81 |     },
 82 |     {
 83 |       "description": "Update an existing user.",
 84 |       "href": "/users/{(%2Fschemata%2Fuser%23%2Fdefinitions%2Fidentity)}",
 85 |       "method": "PATCH",
 86 |       "rel": "update",
 87 |       "schema": {
 88 |         "properties": {
 89 |         },
 90 |         "type": [
 91 |           "object"
 92 |         ]
 93 |       },
 94 |       "title": "Update"
 95 |     }
 96 |   ],
 97 |   "properties": {
 98 |     "created_at": {
 99 |       "$ref": "/schemata/user#/definitions/created_at"
100 |     },
101 |     "id": {
102 |       "$ref": "/schemata/user#/definitions/id"
103 |     },
104 |     "updated_at": {
105 |       "$ref": "/schemata/user#/definitions/updated_at"
106 |     }
107 |   },
108 |   "id": "schemata/user"
109 | }
110 | 


--------------------------------------------------------------------------------
/test/utils_test.rb:
--------------------------------------------------------------------------------
 1 | require_relative "helpers"
 2 | 
 3 | class UtilsTest < Minitest::Test
 4 |   def test_blank?
 5 |     assert_equal true, Prmd::Utils.blank?(nil)
 6 |     assert_equal true, Prmd::Utils.blank?([])
 7 |     assert_equal true, Prmd::Utils.blank?({})
 8 |     assert_equal true, Prmd::Utils.blank?("")
 9 |     assert_equal true, Prmd::Utils.blank?(" ")
10 |     assert_equal true, Prmd::Utils.blank?("       ")
11 |     assert_equal false, Prmd::Utils.blank?([nil])
12 |     assert_equal false, Prmd::Utils.blank?({ a: nil })
13 |     assert_equal false, Prmd::Utils.blank?("A")
14 |     assert_equal false, Prmd::Utils.blank?(Object.new)
15 |   end
16 | end
17 | 


--------------------------------------------------------------------------------