prissy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in prissy.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Paul Sadauskas
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,110 @@
1
+ # Prissy
2
+
3
+ Pretty prints JSON output with even more prettiness, and focus on readability. Also understands things like terminal width, colors, and HTML.
4
+
5
+ ## Why another one?
6
+
7
+ There's plenty of JSON formatters out there, in tons of languages. While none of them are perfect, this one is special because:
8
+
9
+ * Left-align key names. Column-align the values.
10
+ * When nested objects or arrays are short and simple, keep them in the same line as the key. If they're longer, or contain further nestings, then make them multiline.
11
+ * Terminal width aware, so that things don't wrap if we can help it.
12
+ * Colorized terminal output optional.
13
+ * TODO: (colored) HTML output, so we don't have to roundtrip through yet another syntax library.
14
+
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ gem 'prissy'
21
+
22
+ And then execute:
23
+
24
+ $ bundle
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install prissy
29
+
30
+ ## Usage
31
+
32
+ ### From the command line
33
+
34
+ ```
35
+ $ cat example_input.json | prissy
36
+ {
37
+ "array": [
38
+ "one",
39
+ "two",
40
+ "three",
41
+ "four",
42
+ "five",
43
+ "six",
44
+ "seven",
45
+ "eight",
46
+ "nine",
47
+ "ten",
48
+ "eleven",
49
+ "twelve"
50
+ ],
51
+ "false": false,
52
+ "inline_array": ["one", "two", "three"],
53
+ "inline_object": {"foo": "bar"},
54
+ "null": null,
55
+ "number": 42.0,
56
+ "object": {
57
+ "baz": "bax",
58
+ "foo": "bar",
59
+ "more": [1, 2, 3]
60
+ },
61
+ "string": "a text string",
62
+ "true": true
63
+ }
64
+
65
+ $ curl -H "Accept: application/json" https://api.github.com/users/paul | prissy
66
+ {
67
+ "type": "User",
68
+ "avatar_url":
69
+ "https://secure.gravatar.com/avatar/d587890d0fcf8f45724baa8b1bfe1bf4?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png",
70
+ "bio": "",
71
+ "blog": "blog.theamazingrando.com",
72
+ "company": "GitHub",
73
+ "created_at": "2008-02-11T18:44:09Z",
74
+ "email": "psadauskas@gmail.com",
75
+ "followers": 35,
76
+ "following": 1,
77
+ "gravatar_id": "d587890d0fcf8f45724baa8b1bfe1bf4",
78
+ "hireable": false,
79
+ "html_url": "https://github.com/paul",
80
+ "id": 184,
81
+ "location": "Boulder, CO",
82
+ "login": "paul",
83
+ "name": "Paul Sadauskas",
84
+ "public_gists": 322,
85
+ "public_repos": 82,
86
+ "url": "https://api.github.com/users/paul"
87
+ }
88
+ ```
89
+
90
+ ### In Ruby
91
+
92
+ ```ruby
93
+ # The easy way
94
+ Prissy(json_or_hash)
95
+
96
+ # With options
97
+ Prissy.new(width: 120, color: false).prissy(json_or_hash)
98
+ ```
99
+
100
+ ## Output:
101
+
102
+ ![prissy output](https://github-images.s3.amazonaws.com/skitch/2._psadauskas%40Pauls-GitHub-MBP__%7E_Code_personal_prissy_%28zsh%29-20120717-163756.png)
103
+
104
+ ## Contributing
105
+
106
+ 1. Fork it
107
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
108
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
109
+ 4. Push to the branch (`git push origin my-new-feature`)
110
+ 5. Create new Pull Request
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'terminfo'
4
+ $:.unshift(File.expand_path("../../lib", __FILE__))
5
+ require 'prissy'
6
+
7
+
8
+ json = STDIN.read
9
+
10
+ width = TermInfo.screen_width
11
+ has_color = TermInfo.default_object.tigetnum("colors")
12
+
13
+ print Prissy.new(:max_width => width, :color => has_color).prissy(json)
14
+
@@ -0,0 +1,13 @@
1
+
2
+ {
3
+ "inline_array": ["one", "two", "three"],
4
+ "array": [ "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve" ],
5
+ "string": "a text string",
6
+ "number": 42.0,
7
+ "true": true,
8
+ "false": false,
9
+ "null": null,
10
+ "inline_object": {"foo": "bar"},
11
+ "object": {"foo": "bar", "baz": "bax", "more": [1,2,3]}
12
+ }
13
+
@@ -0,0 +1,48 @@
1
+ require "prissy/version"
2
+ require 'prissy/printer'
3
+ require 'prissy/color_printer'
4
+
5
+ require 'multi_json'
6
+
7
+
8
+ class Prissy
9
+ attr_reader :options
10
+
11
+ def initialize(options = {})
12
+ defaults = {}
13
+
14
+ @options = defaults.merge(options)
15
+ end
16
+
17
+ def prissy(json_or_hash)
18
+ hash = (json_or_hash.is_a?(String) ? parse(json_or_hash) : json_or_hash)
19
+
20
+ printer.print(hash)
21
+ end
22
+
23
+ protected
24
+
25
+ def parse(json)
26
+ MultiJson.decode(json)
27
+ end
28
+
29
+ def printer
30
+ @printer ||= begin
31
+ if $0 =~ /prissy$/
32
+ if options[:color]
33
+ ColorPrinter.new(options)
34
+ else
35
+ Printer.new(options)
36
+ end
37
+ else
38
+ Printer.new(options)
39
+ end
40
+ end
41
+ end
42
+
43
+
44
+ end
45
+
46
+ def Prissy(json_or_hash)
47
+ Prissy.new.prissy(json_or_hash)
48
+ end
@@ -0,0 +1,34 @@
1
+ require 'term/ansicolor'
2
+
3
+ class Prissy
4
+ class ColorPrinter < Printer
5
+ include Term::ANSIColor
6
+
7
+ def print_string(string, starting_column)
8
+ super blue{string}
9
+ end
10
+
11
+ def print_number(number)
12
+ super yellow{number.to_s}
13
+ end
14
+
15
+ def print_literal(char)
16
+ super green{char}
17
+ end
18
+
19
+ def print_key(key)
20
+ super cyan{key}
21
+ end
22
+
23
+ def print_true
24
+ @txt << green{"true"}
25
+ end
26
+ def print_false
27
+ @txt << green{"false"}
28
+ end
29
+ def print_null
30
+ @txt << green{"null"}
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,149 @@
1
+ class Prissy
2
+ class Printer
3
+ attr_reader :options
4
+
5
+ def initialize(options = {})
6
+ @txt = ""
7
+ @indent = 0
8
+ @options = options
9
+ options[:max_width] ||= 80
10
+ end
11
+
12
+ def print(object)
13
+ print_object(object)
14
+ @txt << "\n"
15
+ end
16
+
17
+ protected
18
+
19
+ def print_object(object, starting_column = 0)
20
+ if object.empty?
21
+ print_literal '{}'
22
+ elsif object.size == 1
23
+ pair = object.first
24
+ print_literal '{'
25
+ print_pair(pair.first, pair.last)
26
+ print_literal '}'
27
+ else
28
+ max_key_width = object.keys.max_by { |k| k.length }.length
29
+ print_literal '{'
30
+ newline!
31
+ @indent += 1
32
+ object.keys.sort.each do |key|
33
+ value = object[key]
34
+ indent!
35
+ print_pair(key, value, max_key_width)
36
+ print_literal ',' unless key == object.keys.sort.last
37
+ newline!
38
+ end
39
+ @indent -= 1
40
+ indent!
41
+ print_literal '}'
42
+ end
43
+ end
44
+
45
+ def print_pair(key, value, max_key_width = 0)
46
+ print_key(key)
47
+ print_separator
48
+ spacing = max_key_width > 0 ? (max_key_width - key.length) : 0
49
+ @txt << " " * spacing
50
+ print_value(value, @indent * 2 + key.length + 4 + spacing )
51
+ end
52
+
53
+ def print_array(array, starting_column = 0)
54
+ # Length of all values, plus quotes, commas, spaces and brackets
55
+ array_length = calculate_array_length(array)
56
+
57
+ if !array.any?{|el| el.is_a?(Hash) || el.is_a?(Array)} &&
58
+ (starting_column + array_length < options[:max_width])
59
+
60
+ print_literal '['
61
+ array.each do |el|
62
+ print_value(el)
63
+ print_literal ', ' unless el == array.last
64
+ end
65
+ print_literal ']'
66
+ else
67
+ print_literal '['
68
+ newline!
69
+ @indent += 1
70
+ array.each do |el|
71
+ indent!
72
+ print_value(el)
73
+ print_literal ',' unless el == array.last
74
+ newline!
75
+ end
76
+ @indent -= 1
77
+ indent!
78
+ print_literal ']'
79
+ end
80
+
81
+ end
82
+
83
+ def print_key(key)
84
+ @txt << %{"#{key.to_s}"}
85
+ end
86
+
87
+ def print_value(value, starting_column = 0)
88
+ case value
89
+ when Array then print_array(value, starting_column)
90
+ when Hash then print_object(value, starting_column)
91
+ when Numeric then print_number(value)
92
+ when String then print_string(value, starting_column)
93
+ when TrueClass then print_true
94
+ when FalseClass then print_false
95
+ when NilClass then print_null
96
+ end
97
+ end
98
+
99
+ # Make all these explicit, so its easier to override them in subclasses
100
+ def print_string(string, starting_column = 0)
101
+ @txt << %{"#{string}"}
102
+ end
103
+
104
+ def print_number(number)
105
+ @txt << number.to_s
106
+ end
107
+
108
+ def print_true
109
+ @txt << "true"
110
+ end
111
+
112
+ def print_false
113
+ @txt << "false"
114
+ end
115
+
116
+ def print_null
117
+ @txt << "null"
118
+ end
119
+
120
+ def print_separator
121
+ @txt << ": "
122
+ end
123
+
124
+ def print_literal(literal)
125
+ @txt << literal
126
+ end
127
+
128
+ def indent!
129
+ @txt << " " * @indent
130
+ end
131
+
132
+ def newline!
133
+ @txt << "\n"
134
+ end
135
+
136
+ def calculate_array_length(array)
137
+ array.inject(0) do |sum, item|
138
+ case item
139
+ when Numeric, TrueClass, FalseClass, NilClass
140
+ sum += item.to_s.length
141
+ when String
142
+ sum += item.length + 2 # "quotes"
143
+ end
144
+ sum += 2 # ", "
145
+ end
146
+ end
147
+
148
+ end
149
+ end
@@ -0,0 +1,3 @@
1
+ class Prissy
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/prissy/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Paul Sadauskas"]
6
+ gem.email = ["psadauskas@gmail.com"]
7
+ gem.description = %q{Even prettier JSON console output}
8
+ gem.summary = %q{Nicely format JSON}
9
+ gem.homepage = "https://github.com/paul/prissy"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "prissy"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Prissy::VERSION
17
+
18
+ gem.add_development_dependency "rspec", "~> 2.11"
19
+
20
+ gem.add_dependency "multi_json", "~> 1.3"
21
+
22
+ gem.add_dependency "options", "~> 2.3.0"
23
+ gem.add_dependency "term-ansicolor", "~> 1.0.7"
24
+ gem.add_dependency "ruby-terminfo", "~> 0.1.1"
25
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe Prissy, "the easy way" do
4
+ let(:json) { <<-JSON }
5
+ {
6
+ "Image": {
7
+ "Width": 800,
8
+ "Height": 600,
9
+ "Title": "View from 15th Floor",
10
+ "Thumbnail": {
11
+ "Url": "http://www.example.com/image/481989943",
12
+ "Height": 125,
13
+ "Width": "100"
14
+ },
15
+ "IDs": [116, 943, 234, 38793]
16
+ }
17
+ }
18
+ JSON
19
+
20
+ subject { Prissy(json) }
21
+
22
+ it { should look_like(<<-JSON.strip) }
23
+ {"Image": {
24
+ "Height": 600,
25
+ "IDs": [116, 943, 234, 38793],
26
+ "Thumbnail": {
27
+ "Height": 125,
28
+ "Url": "http://www.example.com/image/481989943",
29
+ "Width": "100"
30
+ },
31
+ "Title": "View from 15th Floor",
32
+ "Width": 800
33
+ }}
34
+ JSON
35
+ end
36
+
@@ -0,0 +1,21 @@
1
+ require 'prissy'
2
+
3
+ RSpec.configure do |config|
4
+ config.treat_symbols_as_metadata_keys_with_true_values = true
5
+ config.run_all_when_everything_filtered = true
6
+ config.filter_run :focus
7
+
8
+ # Run specs in random order to surface order dependencies. If you find an
9
+ # order dependency and want to debug it, you can fix the order by providing
10
+ # the seed, which is printed after each run.
11
+ # --seed 1234
12
+ #config.order = 'random'
13
+ end
14
+
15
+ RSpec::Matchers.define(:look_like) do |expected|
16
+ match do |actual|
17
+ actual.strip == expected.strip
18
+ end
19
+
20
+ diffable
21
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+ describe Prissy::Printer do
4
+
5
+ subject { Prissy::Printer.new.print(object) }
6
+
7
+ describe "an empty object" do
8
+ let(:object) { {} }
9
+ it { should look_like '{}' }
10
+ end
11
+
12
+ describe "a simple object" do
13
+ let(:object) { {"foo" => "bar"} }
14
+ it { should look_like '{"foo": "bar"}' }
15
+ end
16
+
17
+ describe "an object with multiple keys" do
18
+ let(:object) { {"foo" => "bar", "x" => "y" } }
19
+ it { should look_like <<-STR.strip }
20
+ {
21
+ "foo": "bar",
22
+ "x": "y"
23
+ }
24
+ STR
25
+ end
26
+
27
+ describe "an object with simple nested objects" do
28
+ let(:object) { {"foo" => "bar", "other" => {"subkey" => "subvalue"}} }
29
+ it { should look_like <<-STR.strip }
30
+ {
31
+ "foo": "bar",
32
+ "other": {"subkey": "subvalue"}
33
+ }
34
+ STR
35
+ end
36
+
37
+ describe "an object with multiple nested objects" do
38
+ let(:object) { {"foo" => "bar", "other" => {"a" => "b", "c" => "d"}} }
39
+ it { should look_like <<-STR.strip }
40
+ {
41
+ "foo": "bar",
42
+ "other": {
43
+ "a": "b",
44
+ "c": "d"
45
+ }
46
+ }
47
+ STR
48
+ end
49
+
50
+ describe "an object with a short array" do
51
+ let(:object) { {"stooges" => %w[Moe Larry Curly] } }
52
+ it { should look_like <<-STR.strip }
53
+ {"stooges": ["Moe", "Larry", "Curly"]}
54
+ STR
55
+ end
56
+
57
+ describe "an object with a long array" do
58
+ let(:object) { {"dwarves" => %w[ Fili Kili Oin Gloin Thorin Dwalin Balin Bifur Bofur Bombur Dori Nori Ori ]} }
59
+ it { should look_like <<-STR.strip }
60
+ {"dwarves": [
61
+ "Fili",
62
+ "Kili",
63
+ "Oin",
64
+ "Gloin",
65
+ "Thorin",
66
+ "Dwalin",
67
+ "Balin",
68
+ "Bifur",
69
+ "Bofur",
70
+ "Bombur",
71
+ "Dori",
72
+ "Nori",
73
+ "Ori"
74
+ ]}
75
+ STR
76
+ end
77
+
78
+ describe "basic types" do
79
+ let(:object) { {
80
+ "string" => "string",
81
+ "number" => 42.0,
82
+ "object" => {"foo" => "bar"},
83
+ "array" => [1, 2, 3],
84
+ "true" => true,
85
+ "false" => false,
86
+ "null" => nil
87
+ } }
88
+ it { should look_like <<-STR.strip }
89
+ {
90
+ "array": [1, 2, 3],
91
+ "false": false,
92
+ "null": null,
93
+ "number": 42.0,
94
+ "object": {"foo": "bar"},
95
+ "string": "string",
96
+ "true": true
97
+ }
98
+ STR
99
+ end
100
+
101
+ end
102
+
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prissy
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Paul Sadauskas
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '2.11'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '2.11'
30
+ - !ruby/object:Gem::Dependency
31
+ name: multi_json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.3'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.3'
46
+ - !ruby/object:Gem::Dependency
47
+ name: options
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 2.3.0
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 2.3.0
62
+ - !ruby/object:Gem::Dependency
63
+ name: term-ansicolor
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 1.0.7
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 1.0.7
78
+ - !ruby/object:Gem::Dependency
79
+ name: ruby-terminfo
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 0.1.1
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 0.1.1
94
+ description: Even prettier JSON console output
95
+ email:
96
+ - psadauskas@gmail.com
97
+ executables:
98
+ - prissy
99
+ extensions: []
100
+ extra_rdoc_files: []
101
+ files:
102
+ - .gitignore
103
+ - .rspec
104
+ - Gemfile
105
+ - LICENSE
106
+ - README.md
107
+ - Rakefile
108
+ - bin/prissy
109
+ - example_input.json
110
+ - lib/prissy.rb
111
+ - lib/prissy/color_printer.rb
112
+ - lib/prissy/printer.rb
113
+ - lib/prissy/version.rb
114
+ - prissy.gemspec
115
+ - spec/acceptance/the_easy_way_spec.rb
116
+ - spec/spec_helper.rb
117
+ - spec/unit/plain_printer_spec.rb
118
+ homepage: https://github.com/paul/prissy
119
+ licenses: []
120
+ post_install_message:
121
+ rdoc_options: []
122
+ require_paths:
123
+ - lib
124
+ required_ruby_version: !ruby/object:Gem::Requirement
125
+ none: false
126
+ requirements:
127
+ - - ! '>='
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ none: false
132
+ requirements:
133
+ - - ! '>='
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ requirements: []
137
+ rubyforge_project:
138
+ rubygems_version: 1.8.23
139
+ signing_key:
140
+ specification_version: 3
141
+ summary: Nicely format JSON
142
+ test_files:
143
+ - spec/acceptance/the_easy_way_spec.rb
144
+ - spec/spec_helper.rb
145
+ - spec/unit/plain_printer_spec.rb