├── .gitignore ├── .rspec ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── yard-contracts.rb └── yard-contracts │ ├── contract_handler.rb │ ├── formatters.rb │ └── version.rb ├── spec ├── nokogiri_wrapper.rb ├── spec_helper.rb ├── yard-contracts │ └── yard-contracts_spec.rb └── yard-test │ ├── custom_contracts.rb │ └── standard_class.rb └── yard-contracts.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | yard-contracts-*.gem 11 | .ruby-version 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.1 4 | - 2.1 5 | - 2.0 6 | - 1.9.3 7 | - jruby-19mode 8 | script: bundle exec rspec 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in yard-contracts.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Simon George 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yard-contracts 2 | 3 | [![Gem Version](https://badge.fury.io/rb/yard-contracts.svg)](http://badge.fury.io/rb/yard-contracts) 4 | [![Code Climate](https://codeclimate.com/github/sfcgeorge/yard-contracts/badges/gpa.svg)](https://codeclimate.com/github/sfcgeorge/yard-contracts) 5 | [![Build Status](https://travis-ci.org/sfcgeorge/yard-contracts.svg?branch=master)](https://travis-ci.org/sfcgeorge/yard-contracts) 6 | [![Inline docs](http://inch-ci.org/github/sfcgeorge/yard-contracts.svg?branch=master)](http://inch-ci.org/github/sfcgeorge/yard-contracts) 7 | [![Join the chat at https://gitter.im/egonSchiele/contracts.ruby](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/egonSchiele/contracts.ruby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | 9 | yard-contracts is a YARD plugin that works with the fantastic [Contracts](https://github.com/egonSchiele/contracts.ruby) gem to automatically document types and descriptions of parameters in your method signatures, saving time, making your code concise and keeping your documentation consistent. 10 | 11 | Have you ever got fed up of coding validations, writing error messages and then documenting those things? All this duplication and boilerplate code has always bugged me. Contracts solves the validations and error messages part already, turning many lines of repetitive code into 1 terse readable one. This extension now solves the documentation part making documentation automatically say the same as your validations. 12 | 13 | * Types gleaned from Contract and automatically linked with the method signature then added to param documentation. 14 | * to_s is called on the types and where it is useful, added to the param documentation as the description. 15 | * If any params are already documented manually, the extra information from the Contract is merged in rather than overriding or duplicating; allowing full flexibility. 16 | 17 | I've used this plugin gem on an existing project and documented 69 methods; it seems to be working great! Note I haven't used all corners of YARD, so there may be advanced features or scenarios that this doesn't work for, please open an issue if you find one. For straightforward projects, however, it's fantastic. 18 | 19 | ## Installation 20 | 21 | Add this line to your application's Gemfile: 22 | 23 | ```ruby 24 | gem 'yard-contracts' 25 | ``` 26 | 27 | And then execute: 28 | 29 | $ bundle 30 | 31 | Or install it yourself as: 32 | 33 | $ gem install yard-contracts 34 | 35 | Compatible with MRI Ruby 2.0+ and JRuby 1.9mode (with additional kramdown gem). 36 | 37 | ## Examples 38 | 39 | See these two equivalent(ish) examples and be blown away. 40 | 41 | Before Contracts and yard-contracts: 42 | 43 | ```ruby 44 | # Find the root of a number, like `Math.sqrt` for arbitrary roots. 45 | # @param root [Numeric] Root must be greater than zero. 46 | # @param num [Numeric] Can't take root of a negative number. 47 | # @return [Numeric] Result will be greater than zero. 48 | def root(root, num) 49 | unless root.is_a?(Numeric) && root > 0 50 | raise "root must be Numeric and > 0." 51 | end 52 | unless num.is_a?(Numeric) && num >= 0 53 | raise "num must be Numeric and >= 0." 54 | end 55 | 56 | result = num**(1.0/root) 57 | 58 | unless result.is_a?(Numeric) && result >= 0 59 | raise "return wasn't Numeric or >= 0." 60 | end 61 | 62 | return result 63 | end 64 | ``` 65 | 66 | After: 67 | 68 | ```ruby 69 | # Find the root of a number, like `Math.sqrt` for arbitrary roots. 70 | # @param num Can't take root of a negative number. 71 | Contract Pos, Nat => Nat 72 | def root(root, num) 73 | num**(1.0/root) 74 | end 75 | ``` 76 | 77 | Isn't that nicer? In the above after example, `@param root ...` and `@return ...` are automatically added to the documentation with their type and no description was deemed necessary. The `num` param has a manual documentation description, that automatically has it's type merged in. This gives you the flexibility to document parameters as explicitly as you like with the minimum of duplication. 78 | 79 | There is another option for adding more detailed parameter descriptions without duplication of the parameter name or the problem of keeping documentation in sync across methods... and it's already built into Contracts: custom type classes with `to_s`! The above example would be a bad place to do this as there's only one method, but imagine your project had lots of methods that calculate roots on numbers or pass those numbers around---you'd end up duplicating the documentation for the `num` parameter. Instead add a custom type class `Rootable` or more explicitly `RootableNum` if you prefer: 80 | 81 | ```ruby 82 | class Rootable 83 | def self.valid? val 84 | Nat.valid? val 85 | end 86 | 87 | def self.to_s 88 | "(Nat) Can't take root of a negative number." 89 | end 90 | end 91 | 92 | # Find the root of a number, like `Math.sqrt` for arbitrary roots. 93 | Contract Pos, Rootable => Rootable 94 | def root(root, num) 95 | num**(1.0/root) 96 | end 97 | ``` 98 | 99 | Wow, the code and docstring is now as terse as possible with no duplication at all. Documentation generated will include the type and custom description as before, and now `Rootable` can be used all throughout the project making everything concise and clear. Plus, run time errors will have the same description from `Rootable` making it easier to debug as your documentation matches your errors. Perfect. 100 | 101 | For yard-contracts to pick up these custom type classes they you need to supply the path to the file they are defined in to YARD with the -e flag, and they must be defined directly under the Contracts namespace or the global namespace. 102 | 103 | See Contracts for more information on creating custom type classes. 104 | 105 | ## Usage 106 | 107 | YARD needs to be made aware of this plugin. Simply give it to YARD with the --plugin flag: 108 | 109 | ``` 110 | bundle exec yardoc --plugin contracts 111 | ``` 112 | 113 | If you have defined custom type classes, they need to be given to YARD as well. First they must be specified in the global namespace or directly under the Contracts namespace. Give YARD the path to your custom contracts with the -e flag: 114 | 115 | ``` 116 | bundle exec yardoc --plugin contracts -e lib/my_project/custom_contracts.rb 117 | ``` 118 | 119 | If using a local fork of yard-contracts you can specify your path to `yard_extensions.rb` with the -e flag instead. 120 | 121 | ``` 122 | bundle exec yardoc -e path/to/lib/yard_extensions.rb 123 | ``` 124 | 125 | ## Development 126 | 127 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment. 128 | 129 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release` to create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 130 | 131 | ## Contributing 132 | 133 | Open issues, and if you can please send pull requests: 134 | 135 | 1. Fork it ( https://github.com/sfcgeorge/yard-contracts/fork ) 136 | 2. Create your feature branch (`git checkout -b my-new-feature`) 137 | 3. Commit your changes (`git commit -am 'Add some feature'`) 138 | 4. Push to the branch (`git push origin my-new-feature`) 139 | 5. Create a new Pull Request 140 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "yard-contracts" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /lib/yard-contracts.rb: -------------------------------------------------------------------------------- 1 | require 'yard-contracts/version' 2 | require File.join(File.dirname(__FILE__), 'yard-contracts', 'contract_handler') 3 | -------------------------------------------------------------------------------- /lib/yard-contracts/contract_handler.rb: -------------------------------------------------------------------------------- 1 | # FIXME: YARD is broken for named arguments in Ruby 2.2, the problem is with 2 | # a Ripper regression in the standard library! Very annoying. 3 | # To use YARD you must downgrade to 2.1 temporarily until 2.2 is patched. 4 | # https://github.com/lsegal/yard/issues/825 5 | require 'yard' 6 | 7 | # require 'contracts/formatters' 8 | require 'contracts/builtin_contracts' 9 | require 'yard-contracts/formatters' 10 | 11 | # Run the plugin handler by supplying it to yard with the --plugin flag 12 | # 13 | # @example 14 | # bundle exec yardoc --plugin contracts 15 | class ContractHandler < YARD::Handlers::Ruby::Base 16 | handles method_call(:Contract) 17 | namespace_only # only match calls inside a namespace not inside a method 18 | 19 | def process 20 | # statement is a YARD attribute, subclassing YARD::Parser::Ruby::AstNode 21 | # Here it's class will be YARD::Parser::Ruby::MethodCallNode 22 | # MethodCallNode#line_range returns the lines the method call was over 23 | # AstNode#line gives the first line of the node 24 | # AstNode#traverse takes a block and yields child nodes 25 | # AstNode#jump returns the first node matching type, otherwise returns self 26 | 27 | # Go up the tree to namespace level, then jump to next def statement 28 | # Note: this won't document dynamicly defined methods. 29 | parent = statement.parent 30 | contract_last_line = statement.line_range.last 31 | # YARD::Parser::Ruby::MethodDefinitionNode 32 | def_method_ast = parent.traverse do |node| 33 | # Find the first def statement that comes after the contract we're on 34 | break node if node.line > contract_last_line && node.def? 35 | end 36 | 37 | ## Hacky way to test for class methods 38 | ## TODO: What about module methods? Probably broken. 39 | scope = def_method_ast.source.match(/def +self\./) ? :class : :instance 40 | name = def_method_ast.method_name true 41 | params = def_method_ast.parameters # YARD::Parser::Ruby::ParameterNode 42 | contracts = statement.parameters # YARD::Parser::Ruby::AstNode 43 | 44 | ret = Contracts::Formatters::ParamContracts.new(params, contracts).return 45 | params = Contracts::Formatters::ParamContracts.new(params, contracts).params 46 | doc = YARD::DocstringParser.new.parse(statement.docstring).to_docstring 47 | 48 | process_params(doc, params) 49 | process_return(doc, ret) 50 | 51 | # YARD hasn't got to the def method yet, so we create a stub of it with 52 | # our docstring, when YARD gets to it properly it will fill in the rest. 53 | YARD::CodeObjects::MethodObject.new(namespace, name, scope) do |o| 54 | o.docstring = doc 55 | end 56 | # No `register()` it breaks stuff! Above implicit return value is enough. 57 | end 58 | 59 | def process_params(doc, params) 60 | merge_params(doc, params) 61 | new_params(doc, params) 62 | end 63 | 64 | def merge_params(doc, params) 65 | # Merge params into provided docstring otherwise there can be duplicates 66 | doc.tags(:param).each do |tag| 67 | next unless (param = params.find { |t| t[0].to_s == tag.name.to_s }) 68 | params.delete(param) 69 | set_tag(tag, param[1], param[2]) 70 | end 71 | end 72 | 73 | def new_params(doc, params) 74 | # If the docstring didn't contain all of the params already add the rest 75 | params.each do |param| 76 | doc.add_tag( 77 | YARD::Tags::Tag.new(:param, param[2].to_s, param[1].inspect, param[0]) 78 | ) 79 | end 80 | end 81 | 82 | def process_return(doc, ret) 83 | if (tag = doc.tag :return) 84 | # Merge return into provided docstring otherwise there can be a duplicate 85 | merge_return(tag, ret) 86 | else 87 | # If the docstring didn't contain a return already add it 88 | new_return(doc, ret) 89 | end 90 | end 91 | 92 | def merge_return(tag, ret) 93 | set_tag(tag, ret[0], ret[1]) 94 | end 95 | 96 | def new_return(doc, ret) 97 | doc.add_tag( 98 | YARD::Tags::Tag.new(:return, ret[1].to_s, ret[0].inspect) 99 | ) 100 | end 101 | 102 | def set_tag(tag, type, to_s) 103 | tag.types ||= [] 104 | tag.types << type.inspect 105 | tag.text = tag_text(to_s, tag.text) 106 | end 107 | 108 | def tag_text(to_s, text) 109 | "#{to_s.empty? ? '' : "#{to_s}. "}#{text}" 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/yard-contracts/formatters.rb: -------------------------------------------------------------------------------- 1 | require 'contracts/builtin_contracts' 2 | 3 | module Contracts 4 | # A namespace for classes related to formatting. 5 | module Formatters 6 | class TypesAST 7 | def initialize(types) 8 | @types = types[0..-2] 9 | end 10 | 11 | def to_a 12 | types = [] 13 | @types.each_with_index do |type, i| 14 | if i == @types.length - 1 15 | # Get the param out of the `param => result` part 16 | types << [type.first.first.source, type.first.first] 17 | else 18 | types << [type.source, type] 19 | end 20 | end 21 | types 22 | end 23 | 24 | def result 25 | # Get the result out of the `param => result` part 26 | [@types.last.last.last.source, @types.last.last.last] 27 | end 28 | end 29 | 30 | class ParamsAST 31 | def initialize(params) 32 | @params = params 33 | end 34 | 35 | def to_a 36 | params = [] 37 | @params.each do |param| 38 | # YARD::Parser::Ruby::AstNode 39 | next if param.nil? 40 | if param.type == :list 41 | param.each do |p| 42 | next if p.nil? 43 | params << build_param_element(p) 44 | end 45 | else 46 | params << build_param_element(param) 47 | end 48 | end 49 | params 50 | end 51 | 52 | private 53 | 54 | def build_param_element(param) 55 | type = param.type 56 | ident = param.jump(:ident, :label).last.to_sym 57 | [type, ident] 58 | end 59 | end 60 | 61 | class TypeAST 62 | def initialize(type) 63 | @type = type 64 | end 65 | 66 | # Formats any type of type. 67 | def type(type = @type) 68 | if type.type == :hash 69 | hash_type(type) 70 | elsif type.type == :array 71 | array_type(type) 72 | else 73 | type.source 74 | end 75 | end 76 | 77 | # Formats Hash type. 78 | def hash_type(hash) 79 | # Ast inherits from Array not Hash so we have to enumerate :assoc nodes 80 | # which are key value pairs of the Hash and build from that. 81 | result = {} 82 | hash.each do |h| 83 | result[h[0].jump(:label).last.to_sym] = 84 | Contracts::Formatters::InspectWrapper.create(type(h[1])) 85 | end 86 | result 87 | end 88 | 89 | # Formats Array type. 90 | def array_type(array) 91 | # This works because Ast inherits from Array. 92 | array.map do |v| 93 | Contracts::Formatters::InspectWrapper.create(type(v)) 94 | end.inspect 95 | end 96 | end 97 | 98 | class ParamContracts 99 | def initialize(param_string, types_string) 100 | @params = ParamsAST.new(param_string).to_a 101 | types = TypesAST.new(types_string) 102 | @types = types.to_a 103 | @result = types.result 104 | end 105 | 106 | def params 107 | s = [] 108 | i = named_count = 0 109 | @params.each do |param| 110 | param_type, param = param 111 | 112 | on_named = param_type == :named_arg || 113 | (named_count > 0 && param_type == :ident) 114 | i -= named_count if on_named 115 | 116 | type, type_ast = @types[i] 117 | con = get_contract_value(type) 118 | type = TypeAST.new(type_ast).type 119 | 120 | # Ripper has :rest_param (splat) but nothing for doublesplat, 121 | # it's just called :ident the same as required positional params. 122 | # This is really annoying. So we have to figure it out. 123 | if on_named 124 | @named_con ||= con 125 | @named_type ||= type 126 | if @named_con.is_a? Hash 127 | if param_type == :named_arg 128 | con = @named_con.delete(param) 129 | type = @named_type.delete(param) 130 | else 131 | con = @named_con 132 | type = @named_type 133 | end 134 | else 135 | @named_con = con = '?' 136 | @named_type = type = [] 137 | end 138 | named_count = 1 139 | end 140 | 141 | type = Contracts::Formatters::InspectWrapper.create(type) 142 | desc = Contracts::Formatters::Expected.new(con, false).contract 143 | # The pluses are to escape things like curly brackets 144 | desc = "#{desc}".empty? ? '' : "+#{desc}+" 145 | s << [param, type, desc] 146 | i += 1 147 | end 148 | s 149 | end 150 | 151 | def return 152 | type, type_ast = @result 153 | con = get_contract_value(type) 154 | type = Contracts::Formatters::InspectWrapper.create( 155 | TypeAST.new(type_ast).type 156 | ) 157 | desc = Contracts::Formatters::Expected.new(con, false).contract 158 | desc = "#{desc}".empty? ? '' : "+#{desc}+" 159 | [type, desc] 160 | end 161 | 162 | private 163 | 164 | # The contract starts as a string, but we need to get it's real value 165 | # so that we can call to_s on it. 166 | def get_contract_value(type) 167 | con = type 168 | begin 169 | con = Contracts.const_get(type) 170 | rescue Exception 171 | begin 172 | con = eval(type) 173 | rescue Exception 174 | end 175 | end 176 | con 177 | end 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /lib/yard-contracts/version.rb: -------------------------------------------------------------------------------- 1 | module YARDContracts 2 | VERSION = '0.1.5' 3 | end 4 | -------------------------------------------------------------------------------- /spec/nokogiri_wrapper.rb: -------------------------------------------------------------------------------- 1 | require 'nokogiri' 2 | 3 | class DocDoc 4 | def initialize(doc); @doc = doc; end 5 | end 6 | 7 | class DocModule < DocDoc 8 | def find_method(instance=:instance, name) 9 | DocMethod.new( 10 | @doc.at_css("##{instance}_method_details") 11 | .at_css("##{name}-#{instance}_method") 12 | .parent 13 | ) 14 | end 15 | 16 | class DocMethod < DocDoc 17 | def discussion 18 | @doc.at_css('.discussion') 19 | end 20 | 21 | def params 22 | @doc.at_css('ul.param') 23 | end 24 | 25 | def param(param) 26 | params.at_css("li:contains('#{param}')") 27 | end 28 | 29 | def return 30 | @doc.at_css('ul.return li') 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'yard-contracts' 3 | -------------------------------------------------------------------------------- /spec/yard-contracts/yard-contracts_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'nokogiri_wrapper' 3 | 4 | describe YARDContracts do 5 | before(:context) do 6 | @base = 7 | if ENV['TRAVIS_BUILD_DIR'] 8 | ENV['TRAVIS_BUILD_DIR'] 9 | else 10 | File.expand_path('../..', File.dirname(__FILE__)) 11 | end 12 | 13 | @yardir = 'yard-spec-output' 14 | Dir.mkdir(@yardir) unless Dir.exist? @yardir 15 | 16 | puts @yard_return = `bundle exec yardoc --quiet --no-highlight --no-save --no-cache --no-stats -o "#{@base}/#{@yardir}" -e "#{@base}/lib/yard-contracts.rb" -e "#{@base}/spec/yard-test/custom_contracts.rb" "#{@base}/spec/yard-test/*.rb"` 17 | 18 | @standard_class_doc = DocModule.new Nokogiri::HTML( 19 | File.read("#{@yardir}/StandardClass.html") 20 | ) 21 | end 22 | 23 | after(:context) do 24 | FileUtils.remove_entry(@yardir) 25 | end 26 | 27 | it 'has a version number' do 28 | expect(YARDContracts::VERSION).not_to be nil 29 | end 30 | 31 | # YARD will err if the plugin isn't loaded correctly 32 | it 'works without YARD failure' do 33 | expect(@yard_return).not_to match(/error/) 34 | end 35 | 36 | # Usual discussion from docstring must be included 37 | it 'still has discussion' do 38 | expect( 39 | @standard_class_doc.find_method(:simple).discussion.text 40 | ).to match(/naming things/) 41 | end 42 | 43 | it 'annotates a param with type' do 44 | expect( 45 | @standard_class_doc.find_method(:simple).param(:one).text 46 | ).to match(/\(Num\)/) 47 | end 48 | 49 | it 'annotates return with type' do 50 | expect( 51 | @standard_class_doc.find_method(:simple).return.text 52 | ).to match(/\(String\)/) 53 | end 54 | 55 | it 'doesnt include useless/duplicate to_s description' do 56 | expect( 57 | @standard_class_doc.find_method(:simple).param(:one).text 58 | ).to_not match(/\(Num\).*Num/) 59 | end 60 | 61 | # Checking that both the type and description are present 62 | it 'calls to_s on complex params' do 63 | ret = @standard_class_doc.find_method(:with_to_s).param(:one).text 64 | expect(ret).to match(/\(Or.+\)/) 65 | expect(ret).to match(/String or Symbol/) 66 | end 67 | 68 | it 'merges types with manual param descriptions' do 69 | ret = @standard_class_doc.find_method(:param_desc).param(:repeats).text 70 | expect(ret).to match(/\(Num\)/) 71 | expect(ret).to match(/times to repeat text/) 72 | end 73 | 74 | it 'merges type with manual return description' do 75 | ret = @standard_class_doc.find_method(:param_desc).return.text 76 | expect(ret).to match(/\(String\)/) 77 | expect(ret).to match(/repeated text/) 78 | end 79 | 80 | it 'merges manual param descriptions with to_s description' do 81 | ret = @standard_class_doc.find_method(:fancy_desc).param(:stringy).text 82 | expect(ret).to match(/\(Or.+\)/) # make sure type is still there 83 | expect(ret).to match(/Symbol or String/) # the to_s part 84 | expect(ret).to match(/what this is/) # custom description 85 | end 86 | 87 | it 'merges manual return description with to_s description' do 88 | ret = @standard_class_doc.find_method(:fancy_desc).return.text 89 | expect(ret).to match(/\(Or.+\)/) 90 | expect(ret).to match(/TrueClass or FalseClass/) 91 | expect(ret).to match(/true for String/) 92 | end 93 | 94 | it 'works for custom contracts with to_s in Contracts namespace' do 95 | ret = @standard_class_doc.find_method(:custom_contract).param(:word).text 96 | expect(ret).to match(/\(Stringy\)/) 97 | expect(ret).to match(/A String or Symbol/) 98 | end 99 | 100 | it 'works for custom contracts with to_s in global namespace' do 101 | ret = @standard_class_doc.find_method(:custom_contract).return.text 102 | expect(ret).to match(/\(Plural\)/) 103 | expect(ret).to match(/A plural String/) 104 | end 105 | 106 | it 'documents class methods from def statement' do 107 | expect( 108 | @standard_class_doc.find_method(:class, :class_simple).param(:bool).text 109 | ).to match(/\(Bool\)/) 110 | end 111 | 112 | it 'documents class methods from def statement with odd formatting' do 113 | expect( 114 | @standard_class_doc.find_method(:class, :class_format).param(:bool).text 115 | ).to match(/\(Bool\)/) 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /spec/yard-test/custom_contracts.rb: -------------------------------------------------------------------------------- 1 | module Contracts 2 | class Stringy 3 | def self.valid?(val) 4 | Or[String, Symbol].valid? val 5 | end 6 | 7 | def self.to_s 8 | 'A String or Symbol' 9 | end 10 | end 11 | end 12 | 13 | class Plural 14 | def self.valid?(val) 15 | val.is_a?(String) && val[-1] == 's' 16 | end 17 | 18 | def self.to_s 19 | "A plural String ending in 's'" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/yard-test/standard_class.rb: -------------------------------------------------------------------------------- 1 | require 'contracts' 2 | 3 | class StandardClass 4 | include Contracts 5 | 6 | # naming things and cache invalidation 7 | Contract Num => String 8 | def simple(one) 9 | self.class_simple(true) 10 | end 11 | 12 | Contract Or[String, Symbol] => Any 13 | def with_to_s(one) 14 | end 15 | 16 | # repeat text number of times 17 | # @param repeats times to repeat text 18 | # @return repeated text 19 | Contract String, Num => String 20 | def param_desc(text, repeats) 21 | end 22 | 23 | # Is it a String or a Symbol? 24 | # @param stringy determine what this is 25 | # @return true for String 26 | Contract Or[Symbol, String] => Or[TrueClass, FalseClass] 27 | def fancy_desc(stringy) 28 | end 29 | 30 | # Custom contracts will be documented (including `to_s`) if they are passed 31 | # to YARD with -e flag and defined under global or Contracts namespace. 32 | Contract Stringy => Plural 33 | def custom_contract(word) 34 | end 35 | 36 | # Class method 37 | Contract Bool => Any 38 | def self.class_simple(bool) 39 | end 40 | 41 | # Class method with odd formatting 42 | Contract Bool => Any 43 | def self.class_format(bool) 44 | end 45 | 46 | # Contract with nested square brackets, breaks Ripper 47 | Contract ArrayOf[ArrayOf[Num]] => Any 48 | def dodgy_brackets(a) 49 | end 50 | 51 | # Contract that gets around Ripper's broken nested square brackets 52 | Contract ArrayOf.new(ArrayOf[Num]) => Any 53 | def hacky_brackets(a) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /yard-contracts.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'yard-contracts/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "yard-contracts" 8 | spec.version = YARDContracts::VERSION 9 | spec.authors = ["Simon George"] 10 | spec.email = ["simon@sfcgeorge.co.uk"] 11 | 12 | spec.summary = %q{YARD Plugin for Automatic Param Docs from Contracts} 13 | spec.description = %q{This YARD plugin uses Contracts and method signatures, 14 | merged with your optional docstring to automatically generate parameter 15 | documentation with type and description. It does the same for return. 16 | } 17 | spec.homepage = "https://github.com/sfcgeorge/yard-contracts" 18 | spec.license = "MIT" 19 | 20 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 21 | spec.require_paths = ["lib"] 22 | 23 | spec.add_dependency "yard", "~> 0.8" 24 | spec.add_dependency "contracts", "~> 0.7" 25 | 26 | spec.add_development_dependency "bundler", "~> 1.6" 27 | spec.add_development_dependency "rake", "~> 10.0" 28 | spec.add_development_dependency "rspec", "~> 3.2" 29 | spec.add_development_dependency "nokogiri", "~> 1.6" 30 | spec.add_development_dependency "kramdown", "~> 1.6" 31 | end 32 | --------------------------------------------------------------------------------