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.
- 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
|