contraption 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.travis.yml +5 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +33 -0
  7. data/Rakefile +12 -0
  8. data/bin/contraption +7 -0
  9. data/contraption.gemspec +29 -0
  10. data/features/author_finalizes_draft.feature +21 -0
  11. data/features/author_generates_site.feature +35 -0
  12. data/features/step_definitions/author_steps.rb +52 -0
  13. data/features/support/env.rb +0 -0
  14. data/features/support/example_inputs.rb +188 -0
  15. data/features/support/hooks.rb +3 -0
  16. data/lib/contraption/catalog.rb +65 -0
  17. data/lib/contraption/formatter.rb +30 -0
  18. data/lib/contraption/header.rb +83 -0
  19. data/lib/contraption/http_handler.rb +11 -0
  20. data/lib/contraption/location.rb +81 -0
  21. data/lib/contraption/options.rb +56 -0
  22. data/lib/contraption/post.rb +69 -0
  23. data/lib/contraption/repository.rb +58 -0
  24. data/lib/contraption/rss_builder.rb +30 -0
  25. data/lib/contraption/runner.rb +60 -0
  26. data/lib/contraption/s3_uploader.rb +30 -0
  27. data/lib/contraption/site.rb +85 -0
  28. data/lib/contraption/tag.rb +20 -0
  29. data/lib/contraption/tag_cloud.rb +35 -0
  30. data/lib/contraption/version.rb +3 -0
  31. data/lib/contraption.rb +14 -0
  32. data/spec/contraption/lib/catalog_spec.rb +105 -0
  33. data/spec/contraption/lib/formatter_spec.rb +30 -0
  34. data/spec/contraption/lib/header_spec.rb +198 -0
  35. data/spec/contraption/lib/location_spec.rb +148 -0
  36. data/spec/contraption/lib/options_spec.rb +25 -0
  37. data/spec/contraption/lib/post_spec.rb +50 -0
  38. data/spec/contraption/lib/repository_spec.rb +38 -0
  39. data/spec/contraption/lib/tag_cloud_spec.rb +39 -0
  40. data/spec/contraption/lib/tag_spec.rb +38 -0
  41. data/spec/contraption/lib/version_spec.rb +9 -0
  42. data/spec/spec_helper.rb +6 -0
  43. metadata +201 -0
@@ -0,0 +1,198 @@
1
+ require_relative '../../spec_helper.rb'
2
+
3
+ require 'date'
4
+
5
+ module Contraption
6
+ describe Header do
7
+ before(:each) do
8
+ @title = "The Title"
9
+ @date = DateTime.now
10
+ @type = :a_type
11
+ @tags = []
12
+ @summary = "some witty line to get people to read the post"
13
+ @h = Header.new title: @title, publication_date: @date, type: @type, tags: @tags, summary: @summary
14
+ end
15
+
16
+ context "initialization" do
17
+
18
+ it "returns the initial title" do
19
+ @h.title.should eq(@title)
20
+ end
21
+
22
+ it "returns the initial date" do
23
+ @h.publication_date.should eq(@date)
24
+ end
25
+
26
+ it "returns the initial type" do
27
+ @h.type.should eq(@type)
28
+ end
29
+
30
+ it "returns the initial tags" do
31
+ @h.tags.should eq(@tags)
32
+ end
33
+
34
+ it "returns the summary" do
35
+ @h.summary.should eq(@summary)
36
+ end
37
+ end
38
+
39
+ context ".new?" do
40
+ it "knows when it is new" do
41
+ h = Header.from "published: now"
42
+ h.new?.should be_true
43
+ end
44
+
45
+ it "knows when it is not new" do
46
+ h = Header.from "published: #{@date}"
47
+ h.new?.should be_false
48
+ end
49
+ end
50
+
51
+ context "#filename" do
52
+ it "stores the filename if specified in the initializer" do
53
+ h = Header.new ({filename: "a-filename.md"})
54
+ h.filename.should eq "a-filename.md"
55
+ end
56
+
57
+ it "returns a filename computed from the title if no filename given" do
58
+ h = Header.new ({title: "A Filename"})
59
+ h.filename.should eq "a-filename.md"
60
+ end
61
+
62
+ it "removes non-alphanumeric characters" do
63
+ h = Header.new ({title: "Call Me ... Maybe?!"})
64
+ h.filename.should eq "call-me-maybe.md"
65
+ end
66
+
67
+ it "does not remove '-'" do
68
+ h = Header.new ({title: "Hyphenated-title"})
69
+ h.filename.should eq "hyphenated-title.md"
70
+ end
71
+
72
+ it "has only one extension" do
73
+ h = Header.new ({filename: "a-filename.md"})
74
+ h.filename.split('.').length.should eq 2
75
+ end
76
+
77
+ it "has an extension when not specified" do
78
+ h = Header.new ({filename: "a-filename"})
79
+ h.filename.split('.').length.should eq 2
80
+ end
81
+ end
82
+
83
+ context "#update" do
84
+ it "returns a new object" do
85
+ update_h = @h.update
86
+ update_h.should_not eq @h
87
+ end
88
+
89
+ it "does not modify the original header" do
90
+ initial_h = @h
91
+ @h.update
92
+ initial_h.should eq @h
93
+ end
94
+
95
+ it "stores the new title" do
96
+ new_h = @h.update title: "New Title"
97
+ new_h.title.should eq "New Title"
98
+ end
99
+
100
+ it "retains the original values" do
101
+ new_h = @h.update
102
+ new_h.title.should eq @h.title
103
+ end
104
+
105
+ it "stores all of the new values" do
106
+ new_h = @h.update title: "New Title", tags: %w[ruby]
107
+ new_h.title.should eq "New Title"
108
+ new_h.tags.should eq %w[ruby]
109
+ end
110
+ end
111
+
112
+ context "::from" do
113
+ let(:title) { "The Title" }
114
+ let(:pub_date) { "Published: Mon, 31 Dec 2012 15:41:00 EST" }
115
+ let(:expected_date) { DateTime.new(2012, 12, 31, 15, 41, 00, '-5') }
116
+ let(:summary) { "SuMMAry: Something witty to get 'eyeballs'" }
117
+ let(:type) { "tyPe: Article" }
118
+ let(:tags) { "TaGS: red, green, blue, purple" }
119
+
120
+ it "returns a Header" do
121
+ h = Header.from ""
122
+ h.class.should eq Header
123
+ end
124
+
125
+ it "finds the title" do
126
+ h = Header.from title
127
+ h.title.should eq title
128
+ end
129
+
130
+ it "finds the filename" do
131
+ h = Header.from "filenAmE: the-filename-goes-here.md"
132
+ h.filename.should eq "the-filename-goes-here.md"
133
+ end
134
+
135
+ it "finds the publication date" do
136
+ h = Header.from pub_date
137
+ h.publication_date.should eq expected_date
138
+ end
139
+
140
+ it "does not manipulate now" do
141
+ h = Header.from "published: now"
142
+ h.publication_date.should eq :now
143
+ end
144
+
145
+ it "finds the summary" do
146
+ h = Header.from summary
147
+ h.summary.should eq "Something witty to get 'eyeballs'"
148
+ end
149
+
150
+ it "finds the type" do
151
+ h = Header.from type
152
+ h.type.should eq :article
153
+ end
154
+
155
+ it "finds the tags" do
156
+ h = Header.from tags
157
+ h.tags.map{|t| t.to_s}.should eq %w[red green blue purple]
158
+ end
159
+
160
+ it "finds multiple elements" do
161
+ combined_input = [title, pub_date, type, tags].join("\n")
162
+ h = Header.from combined_input
163
+ h.title.should eq title
164
+ h.publication_date.should eq expected_date
165
+ h.type.should eq :article
166
+ h.tags.map{|t| t.to_s}.should eq %w[red green blue purple]
167
+ end
168
+
169
+ it "alternate definition for titles" do
170
+ title = "thetitle: and a subtitle"
171
+ h = Header.from("title: " + title)
172
+ h.title.should eq title
173
+ end
174
+
175
+ it "handles initial whitespace" do
176
+ t = "\t A title starting with Whitespace"
177
+ h = Header.from "title: " + t
178
+ h.title.should eq "A title starting with Whitespace"
179
+ end
180
+
181
+ it "handles unsupported header lines" do
182
+ h = Header.from "unsupported: this should not exist"
183
+ expect {h.unsupported}.to raise_error NameError
184
+ end
185
+
186
+ it "ignores '====' separators" do
187
+ example = [title, "="*15].join("\n")
188
+ h = Header.from example
189
+ h.title.should eq title
190
+ end
191
+
192
+ it "finds external links" do
193
+ h = Header.from "external: http://ruby-lang.org"
194
+ h.external.should eq "http://ruby-lang.org"
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,148 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ module Contraption
4
+ describe Location do
5
+ before(:all) { `mkdir /tmp/qscfr` }
6
+ after(:all) { `rm -r /tmp/qscfr` }
7
+
8
+ let (:dir) { "/tmp/qscfr" }
9
+ after(:each) { `rm -rf #{dir}/*` }
10
+
11
+ let (:l) { Location.new dir }
12
+
13
+ it "has a path" do
14
+ l.path.should eq dir
15
+ end
16
+
17
+ it "can create itself on disk" do
18
+ new_location = Location.new(dir + '/newdir')
19
+ new_location.create!
20
+ File.exist?(dir+'/newdir').should be_true
21
+ end
22
+
23
+ context "#list" do
24
+ it "handles empty directories" do
25
+ l.list.should eq []
26
+ end
27
+
28
+ it "handles directories with one file" do
29
+ `touch #{dir}/monkeys`
30
+ l.list.should eq ['monkeys']
31
+ end
32
+
33
+ it "handles directories with multiple files" do
34
+ 10.times {|i| `touch #{dir}/#{i}`}
35
+ l.list.should eq (0..9).map{|i| i.to_s}
36
+ end
37
+
38
+ it "only finds files with specific extension" do
39
+ `touch #{dir}/10.txt #{dir}/10.html #{dir}/10.md #{dir}/10.rb`
40
+ l.list(".html").should eq ["10.html"]
41
+ end
42
+
43
+ it "finds files with any of a list of extensions" do
44
+ `touch #{dir}/10.txt #{dir}/10.html #{dir}/10.md #{dir}/10.rb`
45
+ l.list(%w[.html .md]).should eq %w[10.html 10.md]
46
+ end
47
+ end
48
+
49
+ context "#read" do
50
+ let(:content) { "stuff\nthings" }
51
+ before(:each) { File.open("#{dir}/file", "w:UTF-8") { |f| f.print content } }
52
+
53
+ it "returns the contents of the file" do
54
+ l.read("file").should eq content
55
+ end
56
+
57
+ it "yields to block when passed" do
58
+ l.read("file") do |io|
59
+ io.map {|l| l[0]}
60
+ end.should eq ["s","t"]
61
+ end
62
+
63
+ it "returns a useful token if file does not exist" do
64
+ l.read("nonexistent_file").should eq :file_does_not_exist
65
+ end
66
+
67
+ it "returns a useful token if file is not readable" do
68
+ `touch #{dir}/file && chmod -r #{dir}/file`
69
+ l.read("#{dir}/file").should eq :file_not_readable
70
+ end
71
+ end
72
+
73
+ context "#write" do
74
+ it "writes the provided string to a file" do
75
+ l.write "filename", "this is a test string.\nand another line just for fun."
76
+ end
77
+
78
+ it "does not destructively write to a file" do
79
+ `touch #{dir}/a_new_file`
80
+ l.write("a_new_file", "some content that will not be written").should eq :file_already_exists
81
+ end
82
+ end
83
+
84
+ context "#remove" do
85
+ it "forgets an existing file" do
86
+ l.write("a_new_file", "some stuff to write")
87
+ l.remove("a_new_file")
88
+ l.list.should_not include("a_new_file")
89
+ end
90
+
91
+ it "does nothing for non-existing files" do
92
+ l.remove("a_non_existant_file").should eq :file_does_not_exist
93
+ end
94
+ end
95
+
96
+ context "directory trees" do
97
+ it "recursively creates directories" do
98
+ l.write "some/thing", "something"
99
+ l.read("some/thing").should eq "something"
100
+ end
101
+
102
+ it "recursively lists files" do
103
+ l.write "some/thing", "something"
104
+ l.write "a/new/file", "a"
105
+ l.list.should eq %w[a new file some thing]
106
+ end
107
+ end
108
+
109
+ context "equality" do
110
+ it "returns true for Locations with the same path" do
111
+ l.==(Location.new(dir)).should be_true
112
+ end
113
+
114
+ it "returns false for Locations with different paths" do
115
+ l.==(Location.new(dir+"a")).should be_false
116
+ end
117
+ end
118
+
119
+ context "interrogation" do
120
+ it "contains exist?" do
121
+ l.exist?
122
+ end
123
+
124
+ it "contains readable?" do
125
+ l.readable?
126
+ end
127
+
128
+ it "contains writable?" do
129
+ l.writable?
130
+ end
131
+
132
+ it "contains executable?" do
133
+ l.executable?
134
+ end
135
+ end
136
+
137
+ context "cd" do
138
+ it "returns a new Location rooted at argument" do
139
+ `mkdir #{dir}/subdir`
140
+ l.cd("subdir").path.should eq "#{dir}/subdir"
141
+ end
142
+
143
+ it "creates location if directory does not exist" do
144
+ l.cd("subdir").exist?.should be_true
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,25 @@
1
+ require_relative '../../../lib/contraption/options'
2
+
3
+ module Contraption
4
+ describe Options do
5
+ it "accepts an array of arguments" do
6
+ Options.new []
7
+ end
8
+
9
+ it "sets the source with -s" do
10
+ Options.new(["-s", "TheSource"]).source.path.should eq "TheSource"
11
+ end
12
+
13
+ it "sets the source with --source" do
14
+ Options.new(["--source", "TheSource"]).source.path.should eq "TheSource"
15
+ end
16
+
17
+ it "sets the destination with -d" do
18
+ Options.new(["-d", "TheDestination"]).destination.path.should eq "TheDestination"
19
+ end
20
+
21
+ it "sets the destination with --destination" do
22
+ Options.new(["--destination", "TheDestination"]).destination.path.should eq "TheDestination"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,50 @@
1
+ require_relative '../../spec_helper.rb'
2
+
3
+ module Contraption
4
+ describe Post do
5
+ it "has a body" do
6
+ Post.new.body
7
+ end
8
+
9
+ it "has metadata" do
10
+ Post.new.metadata
11
+ end
12
+
13
+ context "::build" do
14
+ it "returns a post object from the given string" do
15
+ Post.build("testing").class.should eq Post
16
+ end
17
+
18
+ it "has a nonempty title" do
19
+ Post.build("testing").title.should_not eq ""
20
+ end
21
+
22
+ it "has a nonempty body" do
23
+ p = Post.build("testing\n\nsome content for the post")
24
+ p.body.should_not eq ""
25
+ end
26
+
27
+ it "captures the entire contents of multiline posts" do
28
+ p = Post.build("testing\n\nsome content for the post\n\nsome more content")
29
+ p.body.should include "<p>some content for the post</p>\n\n<p>some more content</p>"
30
+ end
31
+
32
+ it "translates markdown to html" do
33
+ p = Post.build("testing\n\n__bold text__")
34
+ p.body.should include "<strong>bold text</strong>"
35
+ end
36
+ end
37
+
38
+ context ".publish" do
39
+ it "sets the new date" do
40
+ p = Post.build "\n\n"
41
+ p = p.publish.publication_date.to_s.should eq Time.now.to_s
42
+ end
43
+
44
+ it "does not modify the body" do
45
+ p = Post.build "\n\nSome stuff"
46
+ p.publish.body.should include "Some stuff"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,38 @@
1
+ require_relative '../../../lib/contraption/repository.rb'
2
+
3
+ module Contraption
4
+ describe Repository do
5
+ context "validity" do
6
+ before(:each) do
7
+ @source = double()
8
+ @source.stub(exist?: true)
9
+ @source.stub(:cd).and_return("")
10
+ end
11
+
12
+ it "raises an error if the structure is invalid" do
13
+ @source.stub(list: %w[drafts pictures])
14
+ expect {r = Repository.new(@source)}.to raise_error
15
+ end
16
+ end
17
+
18
+ it "finds the drafts" do
19
+ Repository.any_instance.stub(:validate)
20
+ source = double()
21
+ source.stub(:list).and_return(%w[test.md house.md])
22
+ source.stub(cd: source)
23
+ r = Repository.new(source)
24
+ r.drafts.should eq %w[test.md house.md]
25
+ end
26
+
27
+ it "returns a collection of posts" do
28
+ Repository.any_instance.stub(:validate)
29
+ source = double()
30
+ source.stub(:list).and_return(%w[test.md house.md])
31
+ source.stub(:read).with("test.md").and_return("something")
32
+ source.stub(:read).with("house.md").and_return("something else")
33
+ source.stub(cd: source)
34
+ r = Repository.new(source)
35
+ r.posts.should respond_to :each
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ require_relative '../../../lib/contraption/tag_cloud'
2
+
3
+ module Contraption
4
+ describe TagCloud do
5
+ context "#initialize" do
6
+ it "takes a collection of posts" do
7
+ tc = TagCloud.new []
8
+ end
9
+ end
10
+
11
+ context "#to_s" do
12
+ before do
13
+ tag1 = double
14
+ tag2 = double
15
+ tag1.stub(:to_s).and_return("Ruby")
16
+ tag1.stub(:to_url).and_return("ruby")
17
+ tag2.stub(:to_s).and_return("OS X")
18
+ tag2.stub(:to_url).and_return("os-x")
19
+ posts = double
20
+ posts.stub(:by_tag).and_return( {tag1 => [1, 2], tag2 => [1]} )
21
+ @tc = TagCloud.new posts
22
+ end
23
+
24
+ it "contains all of the tags" do
25
+ @tc.to_s.should include 'ruby'
26
+ @tc.to_s.should include 'os-x'
27
+ end
28
+
29
+ it "includes a link to the tag page" do
30
+ @tc.to_s.should include 'ruby/index.html'
31
+ @tc.to_s.should include 'os-x/index.html'
32
+ end
33
+
34
+ it "sets the size of each tag" do
35
+ @tc.to_s.should include 'font-size'
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,38 @@
1
+ require_relative '../../../lib/contraption/tag.rb'
2
+
3
+ module Contraption
4
+ describe Tag do
5
+ it "accepts a string to be displayed" do
6
+ t = Tag.new "Ruby"
7
+ end
8
+
9
+ it "returns the display text" do
10
+ t = Tag.new "Ruby"
11
+ t.to_s.should eq "Ruby"
12
+ end
13
+
14
+ context "#to_url" do
15
+ it "provides a url friendly version" do
16
+ t = Tag.new "Ruby"
17
+ t.to_url
18
+ end
19
+
20
+ it "downcases the output" do
21
+ t = Tag.new "Ruby"
22
+ t.to_url.should eq "ruby"
23
+ end
24
+
25
+ it "replaces spaces with dashes" do
26
+ t = Tag.new "ruby rocks"
27
+ t.to_url.should eq "ruby-rocks"
28
+ end
29
+ end
30
+
31
+ context "#to_sym" do
32
+ it "returns a symbolized version of the display text" do
33
+ t = Tag.new "Qwerty"
34
+ t.to_sym.should eq "Qwerty".to_sym
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,9 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ module Contraption
4
+ describe VERSION do
5
+ it "is defined" do
6
+ Contraption::VERSION
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ require_relative '../lib/contraption'
2
+
3
+ if ENV['TRAVIS']
4
+ require 'coveralls'
5
+ Coveralls.wear!
6
+ end