contraption 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +33 -0
- data/Rakefile +12 -0
- data/bin/contraption +7 -0
- data/contraption.gemspec +29 -0
- data/features/author_finalizes_draft.feature +21 -0
- data/features/author_generates_site.feature +35 -0
- data/features/step_definitions/author_steps.rb +52 -0
- data/features/support/env.rb +0 -0
- data/features/support/example_inputs.rb +188 -0
- data/features/support/hooks.rb +3 -0
- data/lib/contraption/catalog.rb +65 -0
- data/lib/contraption/formatter.rb +30 -0
- data/lib/contraption/header.rb +83 -0
- data/lib/contraption/http_handler.rb +11 -0
- data/lib/contraption/location.rb +81 -0
- data/lib/contraption/options.rb +56 -0
- data/lib/contraption/post.rb +69 -0
- data/lib/contraption/repository.rb +58 -0
- data/lib/contraption/rss_builder.rb +30 -0
- data/lib/contraption/runner.rb +60 -0
- data/lib/contraption/s3_uploader.rb +30 -0
- data/lib/contraption/site.rb +85 -0
- data/lib/contraption/tag.rb +20 -0
- data/lib/contraption/tag_cloud.rb +35 -0
- data/lib/contraption/version.rb +3 -0
- data/lib/contraption.rb +14 -0
- data/spec/contraption/lib/catalog_spec.rb +105 -0
- data/spec/contraption/lib/formatter_spec.rb +30 -0
- data/spec/contraption/lib/header_spec.rb +198 -0
- data/spec/contraption/lib/location_spec.rb +148 -0
- data/spec/contraption/lib/options_spec.rb +25 -0
- data/spec/contraption/lib/post_spec.rb +50 -0
- data/spec/contraption/lib/repository_spec.rb +38 -0
- data/spec/contraption/lib/tag_cloud_spec.rb +39 -0
- data/spec/contraption/lib/tag_spec.rb +38 -0
- data/spec/contraption/lib/version_spec.rb +9 -0
- data/spec/spec_helper.rb +6 -0
- metadata +201 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative '../contraption'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module Contraption
|
5
|
+
class Location
|
6
|
+
def initialize path="."
|
7
|
+
@pn = Pathname.new path
|
8
|
+
end
|
9
|
+
|
10
|
+
def list ext=/.*/
|
11
|
+
ext = Array(ext).join('|')
|
12
|
+
ext = Regexp.new ext
|
13
|
+
pn.find
|
14
|
+
.reject { |f| f == pn }
|
15
|
+
.select { |f| ext.match f.extname }
|
16
|
+
.map{ |f| f.basename.to_s }
|
17
|
+
end
|
18
|
+
|
19
|
+
def path
|
20
|
+
pn.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def read file_name
|
24
|
+
complete_name = pn+file_name
|
25
|
+
return :file_does_not_exist unless complete_name.exist?
|
26
|
+
return :file_not_readable unless complete_name.readable?
|
27
|
+
|
28
|
+
return complete_name.read unless block_given?
|
29
|
+
complete_name.open("r") { |f| yield f }
|
30
|
+
end
|
31
|
+
|
32
|
+
def cd directory
|
33
|
+
l = Location.new(pn+directory)
|
34
|
+
l.create! unless l.pn.directory?
|
35
|
+
l
|
36
|
+
end
|
37
|
+
|
38
|
+
def create!
|
39
|
+
pn.mkpath unless exist?
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove file_name
|
43
|
+
complete_name = pn+file_name
|
44
|
+
return :file_does_not_exist unless complete_name.exist?
|
45
|
+
|
46
|
+
complete_name.delete
|
47
|
+
end
|
48
|
+
|
49
|
+
def write file_name, content
|
50
|
+
complete_name = pn+file_name
|
51
|
+
return :file_already_exists if complete_name.exist?
|
52
|
+
|
53
|
+
complete_name.dirname.mkpath unless complete_name.dirname.exist?
|
54
|
+
|
55
|
+
complete_name.open("w:UTF-8") {|f| f.print content}
|
56
|
+
end
|
57
|
+
|
58
|
+
def == other_location
|
59
|
+
path == other_location.path
|
60
|
+
end
|
61
|
+
|
62
|
+
def exist?
|
63
|
+
pn.exist?
|
64
|
+
end
|
65
|
+
|
66
|
+
def readable?
|
67
|
+
pn.readable?
|
68
|
+
end
|
69
|
+
|
70
|
+
def writable?
|
71
|
+
pn.writable?
|
72
|
+
end
|
73
|
+
|
74
|
+
def executable?
|
75
|
+
pn.executable?
|
76
|
+
end
|
77
|
+
|
78
|
+
protected
|
79
|
+
attr_reader :pn
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
require_relative 'location'
|
5
|
+
|
6
|
+
module Contraption
|
7
|
+
|
8
|
+
DEFAULT_SOURCE = "content"
|
9
|
+
DEFAULT_DESTINATION = "public_html"
|
10
|
+
DEFAULT_CONFIG_FILE = Pathname.new("~/.contraption.yaml").expand_path
|
11
|
+
|
12
|
+
class Options
|
13
|
+
attr_reader :values
|
14
|
+
|
15
|
+
def initialize args
|
16
|
+
@values = {
|
17
|
+
source: DEFAULT_SOURCE,
|
18
|
+
destination: DEFAULT_DESTINATION,
|
19
|
+
s3_access_key_id: nil,
|
20
|
+
s3_secret_access_key: nil,
|
21
|
+
s3_target_bucket: nil
|
22
|
+
}
|
23
|
+
@values.merge!(YAML.load_file(DEFAULT_CONFIG_FILE)) if DEFAULT_CONFIG_FILE.file?
|
24
|
+
@values.each_pair {|k, v| @values[k] = Location.new(v) if %i[source destination].include? k}
|
25
|
+
parse(args)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def method_missing m, *a, &b
|
30
|
+
values.fetch(m) { super }
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse args
|
34
|
+
OptionParser.new do |opts|
|
35
|
+
opts.banner = "Usage: contraption [ options ]"
|
36
|
+
opts.on("-d", "--destination path", "Path to destination") do |path|
|
37
|
+
@values[:destination] = Location.new path
|
38
|
+
end
|
39
|
+
opts.on("-s", "--source path", "Path to source") do |path|
|
40
|
+
@values[:source] = Location.new path
|
41
|
+
end
|
42
|
+
opts.on("--generate-config-file", "Create YAML file containing configuration options") do
|
43
|
+
DEFAULT_CONFIG_FILE.open('w+') {|f| f.write(@values.to_yaml) }
|
44
|
+
exit
|
45
|
+
end
|
46
|
+
begin
|
47
|
+
opts.parse!(args)
|
48
|
+
rescue OptionParser::ParseError => e
|
49
|
+
STDERR.puts e.message, "\n", opts
|
50
|
+
exit(1)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require_relative 'header'
|
2
|
+
|
3
|
+
require 'redcarpet'
|
4
|
+
|
5
|
+
module Contraption
|
6
|
+
class Post
|
7
|
+
class << self
|
8
|
+
def build data=""
|
9
|
+
data = data.to_s
|
10
|
+
|
11
|
+
splitted = data.split(/\n\n/, 2)
|
12
|
+
Post.new Header.from(splitted.first), translate_markdown(splitted.last)
|
13
|
+
rescue ArgumentError => e
|
14
|
+
Post.new Header.from(""), ""
|
15
|
+
end
|
16
|
+
|
17
|
+
def translate_markdown content
|
18
|
+
Redcarpet::Markdown.new(Redcarpet::Render::HTML).render(content)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def publish link_handlers=[]
|
23
|
+
links = list_links
|
24
|
+
|
25
|
+
links.map! do |link|
|
26
|
+
h = link_handlers.select{|l| l.protocol == link.first.to_sym}
|
27
|
+
if h.length > 1
|
28
|
+
raise "Multiple link handlers for #{link.first} protocol"
|
29
|
+
elsif h.length == 0
|
30
|
+
nil
|
31
|
+
else
|
32
|
+
h = h.first
|
33
|
+
[link, h.handle(link)]
|
34
|
+
end
|
35
|
+
end.compact!
|
36
|
+
|
37
|
+
update_links links
|
38
|
+
|
39
|
+
Post.new( metadata.update({publication_date: Time.now}), body)
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :body, :metadata
|
43
|
+
private
|
44
|
+
def initialize metadata="", body=""
|
45
|
+
@body = body
|
46
|
+
@metadata = metadata
|
47
|
+
end
|
48
|
+
|
49
|
+
def method_missing m, *a, &b
|
50
|
+
metadata.public_send(m, *a, &b)
|
51
|
+
end
|
52
|
+
|
53
|
+
def list_links
|
54
|
+
links = []
|
55
|
+
body.scan(/="([^"]*)"/) do |link|
|
56
|
+
links << link.first.split('://', 2)
|
57
|
+
end
|
58
|
+
links
|
59
|
+
end
|
60
|
+
|
61
|
+
def update_links new_links
|
62
|
+
new_links.each do |link|
|
63
|
+
orig = link.first.join('://')
|
64
|
+
new = link.last.join('://')
|
65
|
+
@body.gsub!("=\"#{orig}\"", "=\"#{new}\"")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require_relative "post"
|
2
|
+
require_relative "formatter"
|
3
|
+
require_relative "catalog"
|
4
|
+
|
5
|
+
module Contraption
|
6
|
+
class Repository
|
7
|
+
attr_reader :formatter
|
8
|
+
|
9
|
+
def initialize path
|
10
|
+
@path = path
|
11
|
+
validate
|
12
|
+
construct_locations
|
13
|
+
@formatter = Formatter.new @formats_source
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate
|
17
|
+
raise "Invalid source directory structure in #{@path}" unless valid_layout?
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid_layout?
|
21
|
+
@path.exist? and @path.list.include? "drafts" and @path.list.include? "posts"
|
22
|
+
end
|
23
|
+
|
24
|
+
def drafts
|
25
|
+
@drafts_source.list
|
26
|
+
end
|
27
|
+
|
28
|
+
def posts
|
29
|
+
Catalog.new( @posts_source.list.map{|p| Post.build(@posts_source.read p)} )
|
30
|
+
end
|
31
|
+
|
32
|
+
def static_files
|
33
|
+
@static_source.path
|
34
|
+
end
|
35
|
+
|
36
|
+
def completed_drafts
|
37
|
+
drafts.map{|draft| [draft, @drafts_source.read(draft)]}
|
38
|
+
.each_with_object([]) do |draft, result|
|
39
|
+
result << draft.first if Post.build(draft.last).new?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def finalize_completed_drafts link_handlers=[]
|
44
|
+
completed_drafts.map{|draft| [draft, Post.build(@drafts_source.read(draft)).publish(link_handlers)]}
|
45
|
+
.each do |filename, post|
|
46
|
+
@posts_source.write post.publication_date.strftime("%Y%m%d-")+post.filename, @formatter.format(post, :raw)
|
47
|
+
@drafts_source.remove filename
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def construct_locations
|
52
|
+
@drafts_source = @path.cd("drafts")
|
53
|
+
@posts_source = @path.cd("posts")
|
54
|
+
@formats_source = @path.cd("formats")
|
55
|
+
@static_source = @path.cd("static")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rss/maker'
|
2
|
+
|
3
|
+
module Contraption
|
4
|
+
class RSSBuilder
|
5
|
+
def initialize posts
|
6
|
+
@posts = posts
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_rss
|
10
|
+
RSS::Maker.make('2.0') do |maker|
|
11
|
+
maker.channel.author = 'Casey Robinson'
|
12
|
+
maker.channel.updated = Time.now.to_s
|
13
|
+
maker.channel.link = 'http://rampantmonkey.com/rss'
|
14
|
+
maker.channel.title = 'Rampant Monkey - Everything'
|
15
|
+
maker.channel.description = 'Everything published on rampantmonkey.com'
|
16
|
+
|
17
|
+
@posts.most_recent(-1).each do |post|
|
18
|
+
maker.items.new_item do |item|
|
19
|
+
item.link = "http://rampantmonkey.com/#{post.publication_date.strftime('%Y/%m')}/#{post.filename('.html')}"
|
20
|
+
item.title = post.title
|
21
|
+
item.pubDate = post.publication_date.to_s
|
22
|
+
item.updated = post.publication_date.to_s
|
23
|
+
item.description = post.summary
|
24
|
+
item.content_encoded = post.body
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative 'repository'
|
2
|
+
require_relative 'site'
|
3
|
+
require_relative 's3_uploader'
|
4
|
+
require_relative 'http_handler'
|
5
|
+
|
6
|
+
module Contraption
|
7
|
+
class Runner
|
8
|
+
def initialize options
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def run!
|
13
|
+
source.finalize_completed_drafts handlers
|
14
|
+
site.build_all_individuals
|
15
|
+
site.build_month_pages
|
16
|
+
site.build_year_pages
|
17
|
+
site.build_tag_pages
|
18
|
+
site.build_tag_cloud
|
19
|
+
site.build_recent
|
20
|
+
site.build_landing
|
21
|
+
site.build_rss
|
22
|
+
copy_static_files
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def formatter
|
27
|
+
@formatter ||= source.formatter
|
28
|
+
end
|
29
|
+
|
30
|
+
def posts
|
31
|
+
@posts ||= source.posts
|
32
|
+
end
|
33
|
+
|
34
|
+
def site
|
35
|
+
@site ||= Site.new @options.destination, posts, formatter
|
36
|
+
end
|
37
|
+
|
38
|
+
def source
|
39
|
+
@source ||= Repository.new @options.source
|
40
|
+
end
|
41
|
+
|
42
|
+
def copy_static_files
|
43
|
+
`cp -r #{source.static_files}/* #{site.root}` unless (Dir[source.static_files.to_s + "/*"]).length == 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def handlers
|
47
|
+
if @options.s3_access_key_id
|
48
|
+
[
|
49
|
+
HttpHandler.new,
|
50
|
+
S3Uploader.new({access_key_id: @options.s3_access_key_id,
|
51
|
+
secret_access_key: @options.s3_secret_access_key},
|
52
|
+
@options.s3_target_bucket,
|
53
|
+
@options.source)
|
54
|
+
]
|
55
|
+
else
|
56
|
+
[ HttpHandler.new]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'aws/s3'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
module Contraption
|
5
|
+
class S3Uploader
|
6
|
+
def initialize connection_args={}, target_bucket, path
|
7
|
+
AWS::S3::Base.establish_connection! connection_args
|
8
|
+
@target_bucket = target_bucket
|
9
|
+
@path = path.cd "drafts"
|
10
|
+
end
|
11
|
+
|
12
|
+
def protocol
|
13
|
+
:s3
|
14
|
+
end
|
15
|
+
|
16
|
+
def handle request
|
17
|
+
local_path = Pathname.new(@path.path) + request.last
|
18
|
+
if local_path.exist?
|
19
|
+
upload local_path, request.last
|
20
|
+
["http:", "#{@target_bucket}/#{request.last}"]
|
21
|
+
else
|
22
|
+
raise "#{request.last} does not exist"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def upload local_path, remote_path
|
27
|
+
AWS::S3::S3Object.store(remote_path, open(local_path), @target_bucket, :access => :public_read, 'Cache-Control' => 'max-age=3136000')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require_relative 'tag_cloud'
|
2
|
+
require_relative 'rss_builder'
|
3
|
+
|
4
|
+
module Contraption
|
5
|
+
class Site
|
6
|
+
def initialize location, posts, formats
|
7
|
+
@location = location
|
8
|
+
@formats = formats
|
9
|
+
@posts = posts
|
10
|
+
end
|
11
|
+
|
12
|
+
def build_all_individuals
|
13
|
+
@posts.each.to_a.each { |i| build_individual i }
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_individual i
|
17
|
+
post_location = @location.cd(post_path i)
|
18
|
+
context = build i
|
19
|
+
page = @formats.format(context, :page)
|
20
|
+
post_location.write(i.filename('.html'), page)
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_month_pages
|
24
|
+
build_pages_by(:month) {|m| "#{m.year}/#{"%02d" % m.month}"}
|
25
|
+
end
|
26
|
+
|
27
|
+
def build_year_pages
|
28
|
+
build_pages_by(:year) {|y| y.year.to_s}
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_tag_pages
|
32
|
+
build_pages_by(:tag) {|t| "tags/#{Tag.new(t).to_url}"}
|
33
|
+
end
|
34
|
+
|
35
|
+
def build_recent
|
36
|
+
recent_posts = @posts.most_recent 8
|
37
|
+
combined_posts = build recent_posts
|
38
|
+
write_as_page combined_posts, @location, 'recent.html'
|
39
|
+
end
|
40
|
+
|
41
|
+
def build_landing
|
42
|
+
content = @formats.format({most_recent: @posts.most_recent.first}, :landing_page)
|
43
|
+
@location.write('index.html', content)
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_tag_cloud
|
47
|
+
tag_cloud = TagCloud.new @posts
|
48
|
+
content = @formats.format({content: tag_cloud.to_s}, :tag_cloud)
|
49
|
+
path = @location.cd 'tags'
|
50
|
+
write_as_page content, path
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_rss
|
54
|
+
feed = RSSBuilder.new(@posts).to_rss
|
55
|
+
@location.write 'rss', feed
|
56
|
+
end
|
57
|
+
|
58
|
+
def root
|
59
|
+
@location.path
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def build collection
|
64
|
+
Array(collection).map {|p| @formats.format p}.join
|
65
|
+
end
|
66
|
+
|
67
|
+
def build_pages_by type, &determine_path
|
68
|
+
@posts.public_send("by_#{type}".to_sym).each_pair do |key, posts|
|
69
|
+
path = @location.cd determine_path.call key
|
70
|
+
combined_posts = build posts
|
71
|
+
intermediate_page = @formats.format({content: combined_posts, key: key}, type.to_sym)
|
72
|
+
write_as_page intermediate_page, path
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def post_path post
|
77
|
+
post.publication_date.strftime('%Y') + '/' + post.publication_date.strftime('%m')
|
78
|
+
end
|
79
|
+
|
80
|
+
def write_as_page content, path, name='index.html'
|
81
|
+
page = @formats.format(content, :page)
|
82
|
+
path.write(name, page)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Contraption
|
2
|
+
class Tag
|
3
|
+
def initialize text
|
4
|
+
@display_text = text.to_s
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_s
|
8
|
+
@display_text
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_url
|
12
|
+
@display_text.downcase
|
13
|
+
.gsub ' ', '-'
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_sym
|
17
|
+
@display_text.to_sym
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Contraption
|
2
|
+
class TagCloud
|
3
|
+
def initialize posts
|
4
|
+
@posts = posts
|
5
|
+
end
|
6
|
+
|
7
|
+
def to_s
|
8
|
+
@posts.by_tag
|
9
|
+
.each_pair
|
10
|
+
.map {|k, v| [tag_link(k), tag_size(v.length)]}
|
11
|
+
.map {|el| scale el}
|
12
|
+
.join "\n"
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
def tag_link tag
|
17
|
+
tag = Tag.new tag
|
18
|
+
%Q{<a href="tags/#{tag.to_url}/index.html">#{tag.to_s}</a>}
|
19
|
+
end
|
20
|
+
|
21
|
+
def scale element
|
22
|
+
link = element.first
|
23
|
+
size = element.last
|
24
|
+
%Q{<span style="font-size: #{size}em;">#{link}</span>}
|
25
|
+
end
|
26
|
+
|
27
|
+
def max
|
28
|
+
@posts.by_tag.each_pair.map {|k, v| v.length}.max
|
29
|
+
end
|
30
|
+
|
31
|
+
def tag_size length
|
32
|
+
(1 + length.to_f/max) * 0.8
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/contraption.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "contraption/version"
|
2
|
+
require "contraption/post"
|
3
|
+
require "contraption/header"
|
4
|
+
require "contraption/catalog"
|
5
|
+
require "contraption/location"
|
6
|
+
require "contraption/runner"
|
7
|
+
require "contraption/tag"
|
8
|
+
require "contraption/tag_cloud"
|
9
|
+
|
10
|
+
module Contraption
|
11
|
+
Year = Struct.new :year
|
12
|
+
Month = Struct.new :year, :month
|
13
|
+
Day = Struct.new :year, :month, :day
|
14
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require_relative '../../../lib/contraption/catalog'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
module Contraption
|
5
|
+
describe Catalog do
|
6
|
+
context "item storage" do
|
7
|
+
before (:each) do
|
8
|
+
@items = %w[a b c d e f g]
|
9
|
+
@c = Catalog.new @items
|
10
|
+
end
|
11
|
+
|
12
|
+
it "stores items" do
|
13
|
+
@c.items.should eq @items
|
14
|
+
end
|
15
|
+
|
16
|
+
it "stores new items" do
|
17
|
+
@c << "P"
|
18
|
+
@c.items.should eq(@items << "P")
|
19
|
+
end
|
20
|
+
|
21
|
+
it "delegates #each to @items" do
|
22
|
+
@c.each.to_a.should eq @items
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "date grouping" do
|
27
|
+
let(:item_1) { stub(publication_date: Date.new(2012, 2, 1)) }
|
28
|
+
let(:item_2) { stub(publication_date: Date.new(2012, 1, 1)) }
|
29
|
+
let(:item_3) { stub(publication_date: Date.new(2013, 1, 7)) }
|
30
|
+
let(:item_4) { stub(publication_date: Date.new(2013, 1, 8)) }
|
31
|
+
let(:catalog) { Catalog.new [item_1, item_2, item_3, item_4] }
|
32
|
+
|
33
|
+
it "groups items by year" do
|
34
|
+
catalog.by_year.should eq ({
|
35
|
+
Year.new(2012) => [item_1, item_2],
|
36
|
+
Year.new(2013) => [item_3, item_4]
|
37
|
+
})
|
38
|
+
end
|
39
|
+
|
40
|
+
it "groups items by month" do
|
41
|
+
catalog.by_month.should eq ({
|
42
|
+
Month.new(2012, 2) => [item_1],
|
43
|
+
Month.new(2012, 1) => [item_2],
|
44
|
+
Month.new(2013, 1) => [item_3, item_4]
|
45
|
+
})
|
46
|
+
end
|
47
|
+
|
48
|
+
it "groups items by passed block" do
|
49
|
+
catalog.by_day.should eq ({
|
50
|
+
Day.new(2012, 2, 1) => [item_1],
|
51
|
+
Day.new(2012, 1, 1) => [item_2],
|
52
|
+
Day.new(2013, 1, 7) => [item_3],
|
53
|
+
Day.new(2013, 1, 8) => [item_4]
|
54
|
+
})
|
55
|
+
end
|
56
|
+
|
57
|
+
context "#most_recent" do
|
58
|
+
it "returns the most recently published item" do
|
59
|
+
catalog.most_recent.should eq [item_4]
|
60
|
+
end
|
61
|
+
|
62
|
+
it "returns the 3 most recently published items" do
|
63
|
+
catalog.most_recent(3).should eq [item_4, item_3, item_1]
|
64
|
+
end
|
65
|
+
|
66
|
+
it "returns all posts given a negative parameter" do
|
67
|
+
catalog.most_recent(-1).should eq [item_4, item_3, item_1, item_2]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "tag grouping" do
|
73
|
+
let(:item_1) { stub(tags: %w[a b c]) }
|
74
|
+
let(:item_2) { stub(tags: [])}
|
75
|
+
let(:item_3) { stub(tags: %w[a d]) }
|
76
|
+
let(:item_4) { stub(tags: %w[b c d]) }
|
77
|
+
let(:item_5) { stub(tags: %w[c e]) }
|
78
|
+
let(:catalog) { Catalog.new [item_1, item_2, item_3, item_4, item_5] }
|
79
|
+
|
80
|
+
it "groups all items tagged with 'a'" do
|
81
|
+
catalog.by_tag[:a].should eq [item_1, item_3]
|
82
|
+
end
|
83
|
+
|
84
|
+
it "groups all items tagged with 'b'" do
|
85
|
+
catalog.by_tag[:b].should eq [item_1, item_4]
|
86
|
+
end
|
87
|
+
|
88
|
+
it "groups all items tagged with 'c'" do
|
89
|
+
catalog.by_tag[:c].should eq [item_1, item_4, item_5]
|
90
|
+
end
|
91
|
+
|
92
|
+
it "groups all items tagged with 'd'" do
|
93
|
+
catalog.by_tag[:d].should eq [item_3, item_4]
|
94
|
+
end
|
95
|
+
|
96
|
+
it "groups all items tagged with 'e'" do
|
97
|
+
catalog.by_tag[:e].should eq [item_5]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it "includes enumerable" do
|
102
|
+
Catalog.included_modules.should include Enumerable
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require_relative '../../../lib/contraption/formatter'
|
2
|
+
|
3
|
+
module Contraption
|
4
|
+
describe Formatter do
|
5
|
+
it "accepts a source location" do
|
6
|
+
source = "/some/path/"
|
7
|
+
source.stub(:list).and_return([])
|
8
|
+
Formatter.new source
|
9
|
+
end
|
10
|
+
|
11
|
+
context "#formats" do
|
12
|
+
it "contains a list of available formats" do
|
13
|
+
source = double()
|
14
|
+
source.stub(:list).with('.erb').and_return(%w[article.erb link.erb])
|
15
|
+
f = Formatter.new source
|
16
|
+
f.formats.should eq %i[article link]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "#format" do
|
21
|
+
it "raises an error if format does not exist" do
|
22
|
+
source = double()
|
23
|
+
source.stub(:list).and_return([])
|
24
|
+
f = Formatter.new source
|
25
|
+
context = Struct.new(:type).new(:article)
|
26
|
+
expect {f.format(context)}.to raise_error(ArgumentError)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|