diff --git a/lib/puppet/interface/documentation.rb b/lib/puppet/interface/documentation.rb index d0bfbb261..91db0e074 100644 --- a/lib/puppet/interface/documentation.rb +++ b/lib/puppet/interface/documentation.rb @@ -1,172 +1,194 @@ +# This isn't usable outside Puppet::Interface; don't load it alone. class Puppet::Interface - module TinyDocs - attr_accessor :summary - def summary(value = nil) - self.summary = value unless value.nil? - @summary + module DocGen + def self.strip_whitespace(text) + text.gsub!(/[ \t\f]+$/, '') + + # We need to identify an indent: the minimum number of whitespace + # characters at the start of any line in the text. + indent = text.each_line.map {|x| x.index(/[^\s]/) }.compact.min + + if indent > 0 then + text.gsub!(/^[ \t\f]{0,#{indent}}/, '') + end + + return text end - def summary=(value) - value = value.to_s - value =~ /\n/ and - raise ArgumentError, "Face summary should be a single line; put the long text in 'description' instead." - @summary = value + # The documentation attributes all have some common behaviours; previously + # we open-coded them across the set of six things, but that seemed + # wasteful - especially given that they were literally the same, and had + # the same bug hidden in them. + # + # This feels a bit like overkill, but at least the common code is common + # now. --daniel 2011-04-29 + def attr_doc(name, &validate) + # Now, which form of the setter do we want, validated or not? + get_arg = "value.to_s" + if validate + define_method(:"_validate_#{name}", validate) + get_arg = "_validate_#{name}(#{get_arg})" + end + + # We use module_eval, which I don't like much, because we can't have an + # argument to a block with a default value in Ruby 1.8, and I don't like + # the side-effects (eg: no argument count validation) of using blocks + # without as metheds. When we are 1.9 only (hah!) you can totally + # replace this with some up-and-up define_method. --daniel 2011-04-29 + module_eval(<<-EOT, __FILE__, __LINE__ + 1) + def #{name}(value = nil) + self.#{name} = value unless value.nil? + @#{name} + end + + def #{name}=(value) + @#{name} = Puppet::Interface::DocGen.strip_whitespace(#{get_arg}) + end + EOT end + end + + module TinyDocs + extend Puppet::Interface::DocGen - attr_accessor :description - def description(value = nil) - self.description = value unless value.nil? - @description + attr_doc :summary do |value| + value =~ /\n/ and + raise ArgumentError, "Face summary should be a single line; put the long text in 'description' instead." + value end + + attr_doc :description end module FullDocs + extend Puppet::Interface::DocGen include TinyDocs - attr_accessor :examples - def examples(value = nil) - self.examples = value unless value.nil? - @examples - end + attr_doc :examples + attr_doc :notes + attr_doc :license - attr_accessor :short_description + attr_doc :short_description def short_description(value = nil) self.short_description = value unless value.nil? if @short_description.nil? then return nil if @description.nil? lines = @description.split("\n") grab = [5, lines.index('') || 5].min @short_description = lines[0, grab].join("\n") end @short_description end def author(value = nil) unless value.nil? then unless value.is_a? String raise ArgumentError, 'author must be a string; use multiple statements for multiple authors' end if value =~ /\n/ then raise ArgumentError, 'author should be a single line; use multiple statements for multiple authors' end - @authors.push(value) + @authors.push(Puppet::Interface::DocGen.strip_whitespace(value)) end @authors.empty? ? nil : @authors.join("\n") end - def author=(value) - if Array(value).any? {|x| x =~ /\n/ } then - raise ArgumentError, 'author should be a single line; use multiple statements' - end - @authors = Array(value) - end def authors @authors end - def authors=(value) + def author=(value) if Array(value).any? {|x| x =~ /\n/ } then raise ArgumentError, 'author should be a single line; use multiple statements' end - @authors = Array(value) - end - - attr_accessor :notes - def notes(value = nil) - @notes = value unless value.nil? - @notes - end - - attr_accessor :license - def license(value = nil) - @license = value unless value.nil? - @license + @authors = Array(value).map{|x| Puppet::Interface::DocGen.strip_whitespace(x) } end + alias :authors= :author= def copyright(owner = nil, years = nil) if years.nil? and not owner.nil? then raise ArgumentError, 'copyright takes the owners names, then the years covered' end self.copyright_owner = owner unless owner.nil? self.copyright_years = years unless years.nil? if self.copyright_years or self.copyright_owner then "Copyright #{self.copyright_years} by #{self.copyright_owner}" else "Unknown copyright owner and years." end end attr_accessor :copyright_owner def copyright_owner=(value) case value when String then @copyright_owner = value when Array then @copyright_owner = value.join(", ") else raise ArgumentError, "copyright owner must be a string or an array of strings" end @copyright_owner end attr_accessor :copyright_years def copyright_years=(value) years = munge_copyright_year value years = (years.is_a?(Array) ? years : [years]). sort_by do |x| x.is_a?(Range) ? x.first : x end @copyright_years = years.map do |year| if year.is_a? Range then "#{year.first}-#{year.last}" else year end end.join(", ") end def munge_copyright_year(input) case input when Range then input when Integer then if input < 1970 then fault = "before 1970" elsif input > (future = Time.now.year + 2) then fault = "after #{future}" end if fault then raise ArgumentError, "copyright with a year #{fault} is very strange; did you accidentally add or subtract two years?" end input when String then input.strip.split(/,/).map do |part| part = part.strip if part =~ /^\d+$/ then part.to_i elsif found = part.split(/-/) then unless found.length == 2 and found.all? {|x| x.strip =~ /^\d+$/ } raise ArgumentError, "#{part.inspect} is not a good copyright year or range" end Range.new(found[0].to_i, found[1].to_i) else raise ArgumentError, "#{part.inspect} is not a good copyright year or range" end end when Array then result = [] input.each do |item| item = munge_copyright_year item if item.is_a? Array result.concat item else result << item end end result else raise ArgumentError, "#{input.inspect} is not a good copyright year, set, or range" end end end end diff --git a/spec/shared_behaviours/documentation_on_faces.rb b/spec/shared_behaviours/documentation_on_faces.rb index dd2bd3110..3cfb178f7 100644 --- a/spec/shared_behaviours/documentation_on_faces.rb +++ b/spec/shared_behaviours/documentation_on_faces.rb @@ -1,217 +1,256 @@ # encoding: UTF-8 shared_examples_for "documentation on faces" do defined?(Attrs) or Attrs = [:summary, :description, :examples, :short_description, :notes, :author] defined?(SingleLineAttrs) or SingleLineAttrs = [:summary, :author] # Simple, procedural tests that apply to a bunch of methods. Attrs.each do |attr| it "should accept a #{attr}" do expect { subject.send("#{attr}=", "hello") }.not_to raise_error subject.send(attr).should == "hello" end it "should accept a long (single line) value for #{attr}" do text = "I never know when to stop with the word banana" + ("na" * 1000) expect { subject.send("#{attr}=", text) }.to_not raise_error subject.send(attr).should == text end end - # Should they accept multiple lines? - Attrs.each do |attr| - text = "with\nnewlines" - - if SingleLineAttrs.include? attr then - it "should not accept multiline values for #{attr}" do - expect { subject.send("#{attr}=", text) }. - to raise_error ArgumentError, /#{attr} should be a single line/ - subject.send(attr).should be_nil - end - else - it "should accept multiline values for #{attr}" do - expect { subject.send("#{attr}=", text) }.not_to raise_error - subject.send(attr).should == text + Attrs.each do |getter| + setter = "#{getter}=".to_sym + context "#{getter}" do + it "should strip leading whitespace on a single line" do + subject.send(setter, " death to whitespace") + subject.send(getter).should == "death to whitespace" + end + + it "should strip trailing whitespace on a single line" do + subject.send(setter, "death to whitespace ") + subject.send(getter).should == "death to whitespace" + end + + it "should strip whitespace at both ends at once" do + subject.send(setter, " death to whitespace ") + subject.send(getter).should == "death to whitespace" + end + + multiline_text = "with\nnewlines" + if SingleLineAttrs.include? getter then + it "should not accept multiline values" do + expect { subject.send(setter, multiline_text) }. + to raise_error ArgumentError, /#{getter} should be a single line/ + subject.send(getter).should be_nil + end + else + it "should accept multiline values" do + expect { subject.send(setter, multiline_text) }.not_to raise_error + subject.send(getter).should == multiline_text + end + + [1, 2, 4, 7, 25].each do |length| + context "#{length} chars indent" do + indent = ' ' * length + + it "should strip leading whitespace on multiple lines" do + text = "this\nis\the\final\outcome" + subject.send(setter, text.gsub(/^/, indent)) + subject.send(getter).should == text + end + + it "should not remove formatting whitespace, only global indent" do + text = "this\n is\n the\n ultimate\ntest\n" + subject.send(setter, text.gsub(/^/, indent)) + subject.send(getter).should == text + end + end + end + + it "should strip whitespace with a blank line" do + subject.send(setter, " this\n\n should outdent\n") + subject.send(getter).should == "this\n\nshould outdent\n" + end end end end describe "#short_description" do it "should return the set value if set after description" do subject.description = "hello\ngoodbye" subject.short_description = "whatever" subject.short_description.should == "whatever" end it "should return the set value if set before description" do subject.short_description = "whatever" subject.description = "hello\ngoodbye" subject.short_description.should == "whatever" end it "should return nothing if not set and no description" do subject.short_description.should be_nil end it "should return the first paragraph of description if not set (where it is one line long)" do subject.description = "hello" subject.short_description.should == subject.description end it "should return the first paragraph of description if not set (where there is no paragraph break)" do subject.description = "hello\ngoodbye" subject.short_description.should == subject.description end it "should return the first paragraph of description if not set (where there is a paragraph break)" do subject.description = "hello\ngoodbye\n\nmore\ntext\nhere\n\nfinal\nparagraph" subject.short_description.should == "hello\ngoodbye" end it "should trim a very, very long first paragraph" do line = "this is a very, very, very long long line full of text\n" subject.description = line * 20 + "\n\nwhatever, dude." subject.short_description.should == (line * 5).chomp end end describe "multiple authors" do authors = %w{John Paul George Ringo} context "in the DSL" do it "should support multiple authors" do authors.each {|name| subject.author name } subject.authors.should =~ authors subject.author.should == authors.join("\n") end it "should reject author as an array" do expect { subject.author ["Foo", "Bar"] }. to raise_error ArgumentError, /author must be a string/ end end context "#author=" do it "should accept a single name" do subject.author = "Fred" subject.author.should == "Fred" end it "should accept an array of names" do subject.author = authors subject.authors.should =~ authors subject.author.should == authors.join("\n") end it "should not append when set multiple times" do subject.author = "Fred" subject.author = "John" subject.author.should == "John" end it "should reject arrays with embedded newlines" do expect { subject.author = ["Fred\nJohn"] }. to raise_error ArgumentError, /author should be a single line/ end end end describe "#license" do it "should default to reserving rights" do subject.license.should =~ /All Rights Reserved/ end it "should accept an arbitrary license string on the object" do subject.license = "foo" subject.license.should == "foo" end it "should accept symbols to specify existing licenses..." end describe "#copyright" do it "should fail with just a name" do expect { subject.copyright("invalid") }. to raise_error ArgumentError, /copyright takes the owners names, then the years covered/ end [1997, "1997"].each do |year| it "should accept an entity name and a #{year.class.name} year" do subject.copyright("me", year) subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /#{year}/ end it "should accept multiple entity names and a #{year.class.name} year" do subject.copyright ["me", "you"], year subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /\byou\b/ subject.copyright.should =~ /#{year}/ end end ["1997-2003", "1997 - 2003", 1997..2003].each do |range| it "should accept a #{range.class.name} range of years" do subject.copyright("me", range) subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /1997-2003/ end it "should accept a #{range.class.name} range of years" do subject.copyright ["me", "you"], range subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /\byou\b/ subject.copyright.should =~ /1997-2003/ end end [[1997, 2003], ["1997", 2003], ["1997", "2003"]].each do |input| it "should accept the set of years #{input.inspect} in an array" do subject.copyright "me", input subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /1997, 2003/ end it "should accept the set of years #{input.inspect} in an array" do subject.copyright ["me", "you"], input subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /\byou\b/ subject.copyright.should =~ /1997, 2003/ end end it "should warn if someone does math accidentally on the range of years" do expect { subject.copyright "me", 1997-2003 }. to raise_error ArgumentError, /copyright with a year before 1970 is very strange; did you accidentally add or subtract two years\?/ end it "should accept complex copyright years" do years = [1997, 1999, 2000..2002, 2005].reverse subject.copyright "me", years subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /1997, 1999, 2000-2002, 2005/ end end # Things that are automatically generated. [:name, :options, :synopsis].each do |attr| describe "##{attr}" do it "should not allow you to set #{attr}" do subject.should_not respond_to :"#{attr}=" end it "should have a #{attr}" do subject.send(attr).should_not be_nil end it "'s #{attr} should not be empty..." do subject.send(attr).should_not == '' end end end end