nameplate 0.1.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/LICENSE.txt +21 -0
- data/README.md +122 -0
- data/Rakefile +39 -0
- data/lib/nameplate/avatar/cache.rb +59 -0
- data/lib/nameplate/avatar/generator.rb +237 -0
- data/lib/nameplate/avatar/identity.rb +41 -0
- data/lib/nameplate/avatar.rb +37 -0
- data/lib/nameplate/colors/palette.rb +45 -0
- data/lib/nameplate/colors/palettes/custom.rb +20 -0
- data/lib/nameplate/colors/palettes/dracula.rb +29 -0
- data/lib/nameplate/colors/palettes/google.rb +55 -0
- data/lib/nameplate/colors/palettes/iwanthue.rb +234 -0
- data/lib/nameplate/colors/palettes/jedi_light.rb +26 -0
- data/lib/nameplate/colors/palettes/monokai.rb +27 -0
- data/lib/nameplate/colors/palettes/pastel.rb +24 -0
- data/lib/nameplate/colors.rb +42 -0
- data/lib/nameplate/configuration.rb +109 -0
- data/lib/nameplate/fonts/Lato-Light.ttf +0 -0
- data/lib/nameplate/fonts/Roboto-Medium +0 -0
- data/lib/nameplate/has_avatar.rb +33 -0
- data/lib/nameplate/image/resize.rb +111 -0
- data/lib/nameplate/results/failure_result.rb +35 -0
- data/lib/nameplate/results/success_result.rb +27 -0
- data/lib/nameplate/utils/path_helper.rb +18 -0
- data/lib/nameplate/version.rb +5 -0
- data/lib/nameplate/view_helpers/avatar_helper.rb +60 -0
- data/lib/nameplate.rb +57 -0
- data/spec/fixtures/in.png +0 -0
- data/spec/nameplate/avatar/cache_spec.rb +35 -0
- data/spec/nameplate/avatar/generator_spec.rb +108 -0
- data/spec/nameplate/avatar/identity_spec.rb +27 -0
- data/spec/nameplate/colors_spec.rb +34 -0
- data/spec/nameplate/image/resize_spec.rb +96 -0
- data/spec/nameplate/utils/path_helper_spec.rb +16 -0
- data/spec/nameplate/view_helpers/avatar_helper_spec.rb +38 -0
- data/spec/nameplate_spec.rb +102 -0
- data/spec/spec_helper.rb +13 -0
- metadata +111 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NamePlate
|
|
4
|
+
module Results
|
|
5
|
+
# Represents a successful operation result.
|
|
6
|
+
#
|
|
7
|
+
# Provides a consistent API for checking success
|
|
8
|
+
# and accessing the returned value.
|
|
9
|
+
class SuccessResult
|
|
10
|
+
attr_reader :value
|
|
11
|
+
|
|
12
|
+
def initialize(value:)
|
|
13
|
+
@value = value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Boolean]
|
|
17
|
+
def success?
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [Boolean]
|
|
22
|
+
def failure?
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Utils
|
|
4
|
+
module PathHelper
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
# Convert a public file-system path to a URL path.
|
|
8
|
+
#
|
|
9
|
+
# @param path [String, Pathname]
|
|
10
|
+
# @return [String]
|
|
11
|
+
#
|
|
12
|
+
# Examples:
|
|
13
|
+
# PathHelper.path_to_url("public/avatars/a.png") #=> "/avatars/a.png"
|
|
14
|
+
def path_to_url(path)
|
|
15
|
+
path.to_s.sub(%r{\Apublic/}, "/")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_view"
|
|
4
|
+
require "action_view/helpers"
|
|
5
|
+
|
|
6
|
+
module NamePlate
|
|
7
|
+
module ViewHelpers
|
|
8
|
+
# Rails view helpers for rendering avatars in controllers/views.
|
|
9
|
+
#
|
|
10
|
+
# Usage in Rails:
|
|
11
|
+
# include NamePlate::ViewHelpers::Avatar
|
|
12
|
+
#
|
|
13
|
+
# Then in your views:
|
|
14
|
+
# nameplate_for("Tony", 200)
|
|
15
|
+
# nameplate_url("Tony", 200)
|
|
16
|
+
# nameplate_tag("Tony", 200, class: "avatar")
|
|
17
|
+
module AvatarHelper
|
|
18
|
+
if defined?(ActionView::Helpers::AssetTagHelper)
|
|
19
|
+
include ActionView::Helpers::AssetTagHelper
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Return path to generated avatar image
|
|
23
|
+
#
|
|
24
|
+
# @param name [String] the name to base avatar on
|
|
25
|
+
# @param size [Integer] requested size in px
|
|
26
|
+
# @return [String] filesystem path
|
|
27
|
+
def nameplate_for(name, size = 64)
|
|
28
|
+
NamePlate::Avatar.generate(name, size)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Return URL for generated avatar image
|
|
32
|
+
#
|
|
33
|
+
# @param name [String] the name to base avatar on
|
|
34
|
+
# @param size [Integer] requested size in px
|
|
35
|
+
# @return [String] URL path
|
|
36
|
+
def nameplate_url(name, size = 64)
|
|
37
|
+
NamePlate.path_to_url(nameplate_for(name, size))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Render an <img> tag for the avatar
|
|
41
|
+
#
|
|
42
|
+
# @param name [String] the name to base avatar on
|
|
43
|
+
# @param size [Integer] requested size
|
|
44
|
+
# @param options [Hash] HTML options (e.g., :class)
|
|
45
|
+
# @return [String] HTML img tag
|
|
46
|
+
def nameplate_tag(name, size = 64, options = {})
|
|
47
|
+
src = nameplate_url(name, size)
|
|
48
|
+
|
|
49
|
+
if defined?(ActionView::Helpers::AssetTagHelper)
|
|
50
|
+
extend ActionView::Helpers::AssetTagHelper
|
|
51
|
+
image_tag(src, options.merge(alt: name))
|
|
52
|
+
else
|
|
53
|
+
class_attr = options.fetch(:class, nil)
|
|
54
|
+
class_str = class_attr ? %( class="#{class_attr}") : ""
|
|
55
|
+
%(<img alt="#{name}"#{class_str} src="#{src}" />)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/nameplate.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# stdlib dependencies used internally
|
|
4
|
+
require "open3"
|
|
5
|
+
require_relative "nameplate/version"
|
|
6
|
+
require_relative "nameplate/configuration"
|
|
7
|
+
require_relative "nameplate/results/success_result"
|
|
8
|
+
require_relative "nameplate/results/failure_result"
|
|
9
|
+
require_relative "nameplate/avatar"
|
|
10
|
+
require_relative "nameplate/colors"
|
|
11
|
+
require_relative "nameplate/has_avatar"
|
|
12
|
+
require_relative "nameplate/image/resize"
|
|
13
|
+
require_relative "nameplate/utils/path_helper"
|
|
14
|
+
require_relative "nameplate/view_helpers/avatar_helper"
|
|
15
|
+
|
|
16
|
+
module NamePlate
|
|
17
|
+
extend NamePlate::Configuration
|
|
18
|
+
|
|
19
|
+
# Setup DSL for configuration
|
|
20
|
+
#
|
|
21
|
+
# Example:
|
|
22
|
+
# NamePlate.setup do |config|
|
|
23
|
+
# config.cache_base_path = "public/system"
|
|
24
|
+
# config.colors_palette = :dracula
|
|
25
|
+
# end
|
|
26
|
+
def self.setup
|
|
27
|
+
yield(self)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Public API: generate avatar for a given username
|
|
31
|
+
#
|
|
32
|
+
# @param username [String]
|
|
33
|
+
# @param size [Integer]
|
|
34
|
+
# @return [String] path to generated avatar
|
|
35
|
+
def self.generate(username, size)
|
|
36
|
+
Avatar.generate(username, size)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Resize an image and return a structured result
|
|
40
|
+
#
|
|
41
|
+
# @param from [String] source path
|
|
42
|
+
# @param to [String] destination path
|
|
43
|
+
# @param width [Integer]
|
|
44
|
+
# @param height [Integer]
|
|
45
|
+
# @return [SuccessResult, FailureResult]
|
|
46
|
+
def self.resize_image(from:, to:, width:, height:)
|
|
47
|
+
Image::Resize.call(from:, to:, width:, height:)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Convert a filesystem path to a URL
|
|
51
|
+
#
|
|
52
|
+
# @param path [String, Pathname]
|
|
53
|
+
# @return [String]
|
|
54
|
+
def self.path_to_url(path)
|
|
55
|
+
Utils::PathHelper.path_to_url(path)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe NamePlate::Avatar::Cache do
|
|
6
|
+
let(:tmpdir) { File.expand_path("../../tmp/cache", __dir__) }
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
FileUtils.rm_rf(tmpdir)
|
|
10
|
+
FileUtils.mkdir_p(tmpdir)
|
|
11
|
+
@orig = NamePlate.cache_base_path
|
|
12
|
+
NamePlate.cache_base_path = tmpdir
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
after do
|
|
16
|
+
NamePlate.cache_base_path = @orig
|
|
17
|
+
FileUtils.rm_rf(tmpdir)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
let(:identity) { NamePlate::Avatar::Identity.new([226, 95, 81], "T") }
|
|
21
|
+
|
|
22
|
+
describe ".base_path" do
|
|
23
|
+
it "builds base path with version" do
|
|
24
|
+
expect(described_class.base_path).to eq(File.join(tmpdir, "nameplate", NamePlate::Avatar::VERSION.to_s))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe ".path" do
|
|
29
|
+
it "creates directories and returns full file path" do
|
|
30
|
+
path = described_class.path(identity, 64)
|
|
31
|
+
expect(path).to end_with("/T/226_95_81/64.png")
|
|
32
|
+
expect(File).to exist(File.dirname(path))
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe NamePlate::Avatar::Generator do
|
|
6
|
+
let(:identity) { NamePlate::Avatar::Identity.new([226, 95, 81], "T") }
|
|
7
|
+
let(:target_path) { File.expand_path("../../tmp/generated/64.png", __dir__) }
|
|
8
|
+
let(:fake_font) { "/fake/font.ttf" }
|
|
9
|
+
let(:mm_cmd) do
|
|
10
|
+
double("MiniMagick::Command",
|
|
11
|
+
:size => nil, :<< => nil, :pointsize => nil, :font => nil,
|
|
12
|
+
:weight => nil, :fill => nil, :gravity => nil, :annotate => nil)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
before do
|
|
16
|
+
# Never touch the filesystem
|
|
17
|
+
allow(FileUtils).to receive(:mkdir_p)
|
|
18
|
+
allow(FileUtils).to receive(:rm_f)
|
|
19
|
+
|
|
20
|
+
# Stub MiniMagick constant if not loaded
|
|
21
|
+
stub_const("MiniMagick", Module.new) unless defined?(MiniMagick)
|
|
22
|
+
|
|
23
|
+
# Avoid any real font lookups
|
|
24
|
+
allow(NamePlate).to receive(:font).and_return(fake_font)
|
|
25
|
+
allow(File).to receive(:exist?).and_call_original
|
|
26
|
+
allow(File).to receive(:exist?).with(fake_font).and_return(true)
|
|
27
|
+
|
|
28
|
+
# Deterministic identity + cache path
|
|
29
|
+
allow(NamePlate::Avatar::Identity).to receive(:from_username).and_return(identity)
|
|
30
|
+
allow(NamePlate::Avatar::Cache).to receive(:path).and_return(target_path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe ".call" do
|
|
34
|
+
it "returns cached path when file exists and cache=true" do
|
|
35
|
+
allow(File).to receive(:exist?).with(target_path).and_return(true)
|
|
36
|
+
|
|
37
|
+
expect(described_class.call("Tony", 64, cache: true)).to eq(target_path)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "generates via MiniMagick when cache miss" do
|
|
41
|
+
allow(File).to receive(:exist?).with(target_path).and_return(false)
|
|
42
|
+
expect(MiniMagick).to receive(:convert).and_yield(mm_cmd)
|
|
43
|
+
|
|
44
|
+
expect(described_class.call("Tony", 64, cache: false)).to eq(target_path)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "caps size at Avatar::FULLSIZE" do
|
|
48
|
+
allow(File).to receive(:exist?).with(target_path).and_return(false)
|
|
49
|
+
allow(MiniMagick).to receive(:convert).and_yield(mm_cmd)
|
|
50
|
+
|
|
51
|
+
expect(NamePlate::Avatar::Cache).to receive(:path) do |_, s|
|
|
52
|
+
expect(s).to eq(NamePlate::Avatar::FULLSIZE)
|
|
53
|
+
target_path
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
described_class.call("Tony", NamePlate::Avatar::FULLSIZE + 100, cache: false)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe ".async_call" do
|
|
61
|
+
before do
|
|
62
|
+
# Always simulate cache miss
|
|
63
|
+
allow(File).to receive(:exist?).with(target_path).and_return(false)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
let(:future) { described_class.async_call("Tony", 64) }
|
|
67
|
+
|
|
68
|
+
def stub_minimagick_success
|
|
69
|
+
allow(MiniMagick).to receive(:convert).and_yield(mm_cmd)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def stub_minimagick_failure(msg = "boom")
|
|
73
|
+
allow(MiniMagick).to receive(:convert).and_raise(msg)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "returns a future that resolves to the avatar path" do
|
|
77
|
+
stub_minimagick_success
|
|
78
|
+
expect(future.value!).to eq(target_path)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "wraps MiniMagick errors in ImageMagickError" do
|
|
82
|
+
stub_minimagick_failure
|
|
83
|
+
expect { future.value! }.to raise_error(described_class::ImageMagickError, /MiniMagick failed/)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "wraps filesystem errors in FileSystemError" do
|
|
87
|
+
allow(NamePlate::Avatar::Cache).to receive(:path).and_raise(Errno::EACCES)
|
|
88
|
+
expect { future.value! }.to raise_error(described_class::FileSystemError, /Permission denied/)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "wraps unexpected errors in GenerationError" do
|
|
92
|
+
allow(NamePlate::Avatar::Identity).to receive(:from_username).and_raise("weird failure")
|
|
93
|
+
expect { future.value! }.to raise_error(described_class::GenerationError, /Unexpected error/)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe "input validation" do
|
|
98
|
+
[
|
|
99
|
+
{input: [" ", 64], error: described_class::ConfigurationError, description: "blank username"},
|
|
100
|
+
{input: ["Tony", 0], error: described_class::ConfigurationError, description: "non-positive size"},
|
|
101
|
+
{input: ["Tony", -5], error: described_class::ConfigurationError, description: "negative size"}
|
|
102
|
+
].each do |test_case|
|
|
103
|
+
it "raises #{test_case[:error]} for #{test_case[:description]}" do
|
|
104
|
+
expect { described_class.call(*test_case[:input]) }.to raise_error(test_case[:error])
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe NamePlate::Avatar::Identity do
|
|
6
|
+
describe ".from_username" do
|
|
7
|
+
before do
|
|
8
|
+
allow(NamePlate::Colors).to receive(:for).and_return([1, 2, 3])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it "uses first letter for single-word usernames" do
|
|
12
|
+
identity = described_class.from_username("tony")
|
|
13
|
+
expect(identity.letters).to eq("T")
|
|
14
|
+
expect(identity.color).to eq([1, 2, 3])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "uses two letters for multi-word usernames" do
|
|
18
|
+
identity = described_class.from_username("Tony Lombardi")
|
|
19
|
+
expect(identity.letters).to eq("TL")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "upcases letters and trims whitespace" do
|
|
23
|
+
identity = described_class.from_username(" ada ")
|
|
24
|
+
expect(identity.letters).to eq("A")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe NamePlate::Colors do
|
|
6
|
+
describe ".valid_custom_palette?" do
|
|
7
|
+
it "returns true for a valid palette of hex colors (2..20)" do
|
|
8
|
+
palette = ["#fff", "#a1b2c3", "#123456"]
|
|
9
|
+
expect(described_class.valid_custom_palette?(palette)).to be(true)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "returns false for nil" do
|
|
13
|
+
expect(described_class.valid_custom_palette?(nil)).to be(false)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "returns false for non-array" do
|
|
17
|
+
expect(described_class.valid_custom_palette?("#fff")).to be(false)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "returns false for invalid hex strings" do
|
|
21
|
+
expect(described_class.valid_custom_palette?(["fff", "#12"])).to be(false)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "returns false when too few colors" do
|
|
25
|
+
expect(described_class.valid_custom_palette?(["#fff"]))
|
|
26
|
+
.to be(false)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "returns false when too many colors" do
|
|
30
|
+
palette = Array.new(21, "#fff")
|
|
31
|
+
expect(described_class.valid_custom_palette?(palette)).to be(false)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe NamePlate::Image::Resize do
|
|
6
|
+
let(:src) { File.expand_path("../../fixtures/src.png", __dir__) }
|
|
7
|
+
let(:dst) { File.expand_path("../../tmp/out.png", __dir__) }
|
|
8
|
+
let(:width) { 200 }
|
|
9
|
+
let(:height) { 200 }
|
|
10
|
+
|
|
11
|
+
before do
|
|
12
|
+
FileUtils.mkdir_p(File.dirname(src))
|
|
13
|
+
File.write(src, "PNG") unless File.exist?(src)
|
|
14
|
+
|
|
15
|
+
FileUtils.mkdir_p(File.dirname(dst))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
after do
|
|
19
|
+
FileUtils.rm_f(src)
|
|
20
|
+
FileUtils.rm_f(dst)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe ".call" do
|
|
24
|
+
context "when source does not exist" do
|
|
25
|
+
it "returns FailureResult" do
|
|
26
|
+
result = described_class.call(from: "missing.png", to: dst, width: 10, height: 10)
|
|
27
|
+
|
|
28
|
+
expect(result).to be_a(NamePlate::Results::FailureResult)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
context "when width is not positive" do
|
|
33
|
+
it "returns FailureResult" do
|
|
34
|
+
result = described_class.call(from: src, to: dst, width: 0, height: 10)
|
|
35
|
+
|
|
36
|
+
expect(result.error[:message]).to include("Width must be positive integer")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
context "when height is not positive" do
|
|
41
|
+
it "returns FailureResult" do
|
|
42
|
+
result = described_class.call(from: src, to: dst, width: 10, height: 0)
|
|
43
|
+
|
|
44
|
+
expect(result.error[:message]).to include("Height must be positive integer")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
context "when destination path is empty" do
|
|
49
|
+
it "returns FailureResult" do
|
|
50
|
+
result = described_class.call(from: src, to: " ", width: 10, height: 10)
|
|
51
|
+
|
|
52
|
+
expect(result.error[:message]).to include("Destination path cannot be empty")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
context "when MiniMagick processes successfully" do
|
|
57
|
+
let(:image_double) do
|
|
58
|
+
instance_double(MiniMagick::Image, combine_options: nil, write: true)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
let(:command_double) do
|
|
62
|
+
double("MiniMagick::Command").tap do |d|
|
|
63
|
+
allow(d).to receive(:background)
|
|
64
|
+
allow(d).to receive(:gravity)
|
|
65
|
+
allow(d).to receive(:thumbnail)
|
|
66
|
+
allow(d).to receive(:extent)
|
|
67
|
+
allow(d).to receive(:unsharp)
|
|
68
|
+
allow(d).to receive(:quality)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
before do
|
|
73
|
+
allow(MiniMagick::Image).to receive(:open).with(src).and_return(image_double)
|
|
74
|
+
allow(image_double).to receive(:combine_options).and_yield(command_double)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "returns SuccessResult with the destination path" do
|
|
78
|
+
result = described_class.call(from: src, to: dst, width: 80, height: 60)
|
|
79
|
+
|
|
80
|
+
expect(result.value[:path]).to eq(dst)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
context "when MiniMagick raises an error" do
|
|
85
|
+
before do
|
|
86
|
+
allow(MiniMagick::Image).to receive(:open).and_raise(StandardError, "boom")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "returns FailureResult with error details" do
|
|
90
|
+
result = described_class.call(from: src, to: dst, width: 80, height: 60)
|
|
91
|
+
|
|
92
|
+
expect(result.error[:message]).to include("Image resize failed: boom")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Utils::PathHelper do
|
|
6
|
+
describe ".path_to_url" do
|
|
7
|
+
it "strips leading public/ and prefixes with /" do
|
|
8
|
+
expect(described_class.path_to_url("public/system/nameplate/1/T/226_95_81/64.png"))
|
|
9
|
+
.to eq("/system/nameplate/1/T/226_95_81/64.png")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "returns string unchanged if it does not start with public/" do
|
|
13
|
+
expect(described_class.path_to_url("/system/nameplate/a.png")).to eq("/system/nameplate/a.png")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe NamePlate::ViewHelpers::AvatarHelper do
|
|
6
|
+
let(:dummy_view) do
|
|
7
|
+
Class.new do
|
|
8
|
+
include NamePlate::ViewHelpers::AvatarHelper
|
|
9
|
+
end.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
allow(NamePlate::Avatar).to receive(:generate).and_return("public/system/nameplate/1/T/226_95_81/64.png")
|
|
14
|
+
allow(NamePlate).to receive(:path_to_url).and_call_original
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe "#nameplatefor" do
|
|
18
|
+
it "delegates to NamePlate::Avatar.generate" do
|
|
19
|
+
expect(NamePlate::Avatar).to receive(:generate).with("Tony", 64)
|
|
20
|
+
dummy_view.nameplate_for("Tony", 64)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "#nameplate_url" do
|
|
25
|
+
it "returns URL path derived from generated path" do
|
|
26
|
+
url = dummy_view.nameplate_url("Tony", 64)
|
|
27
|
+
expect(url).to eq("/system/nameplate/1/T/226_95_81/64.png")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe "#nameplate_tag" do
|
|
32
|
+
it "renders a plain img tag when AssetTagHelper is not defined" do
|
|
33
|
+
hide_const("ActionView::Helpers::AssetTagHelper") if defined?(ActionView::Helpers::AssetTagHelper)
|
|
34
|
+
html = dummy_view.nameplate_tag("Tony", 64, class: "avatar")
|
|
35
|
+
expect(html).to eq('<img alt="Tony" class="avatar" src="/system/nameplate/1/T/226_95_81/64.png" />')
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe NamePlate do
|
|
6
|
+
describe ".setup" do
|
|
7
|
+
around do |example|
|
|
8
|
+
original = {
|
|
9
|
+
cache_base_path: described_class.cache_base_path,
|
|
10
|
+
pointsize: described_class.pointsize,
|
|
11
|
+
weight: described_class.weight,
|
|
12
|
+
annotate_pos: described_class.annotate_position
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
example.run
|
|
16
|
+
ensure
|
|
17
|
+
described_class.setup do |c|
|
|
18
|
+
c.cache_base_path = original[:cache_base_path]
|
|
19
|
+
c.pointsize = original[:pointsize]
|
|
20
|
+
c.weight = original[:weight]
|
|
21
|
+
c.annotate_position = original[:annotate_pos]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "yields the NamePlate module" do
|
|
26
|
+
expect { |blk| described_class.setup(&blk) }.to yield_with_args(described_class)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "applies configuration set inside the block" do
|
|
30
|
+
described_class.setup { |c| c.cache_base_path = "tmp/system" }
|
|
31
|
+
expect(described_class.cache_base_path).to eq("tmp/system")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe ".generate" do
|
|
36
|
+
it "delegates to Avatar.generate with the same arguments" do
|
|
37
|
+
expect(NamePlate::Avatar).to receive(:generate).with("Tony", 128).and_return("path/to/avatar.png")
|
|
38
|
+
described_class.generate("Tony", 128)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "returns the value from Avatar.generate" do
|
|
42
|
+
allow(NamePlate::Avatar).to receive(:generate).and_return("path/to/avatar.png")
|
|
43
|
+
expect(described_class.generate("Tony", 128)).to eq("path/to/avatar.png")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe ".resize_image" do
|
|
48
|
+
let(:src) { File.expand_path("../../fixtures/src.png", __dir__) }
|
|
49
|
+
let(:dst) { File.expand_path("../../tmp/out.png", __dir__) }
|
|
50
|
+
let(:width) { 200 }
|
|
51
|
+
let(:height) { 200 }
|
|
52
|
+
let(:resize) { NamePlate::Image::Resize }
|
|
53
|
+
let(:success) { NamePlate::Results::SuccessResult.new(value: {path: "out.png"}) }
|
|
54
|
+
|
|
55
|
+
before do
|
|
56
|
+
FileUtils.mkdir_p(File.dirname(src))
|
|
57
|
+
File.write(src, "PNG") unless File.exist?(src)
|
|
58
|
+
|
|
59
|
+
FileUtils.mkdir_p(File.dirname(dst))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
after do
|
|
63
|
+
FileUtils.rm_f(src)
|
|
64
|
+
FileUtils.rm_f(dst)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "instantiates Image::Resize and calls #resize with keyword args" do
|
|
68
|
+
expect(resize).to receive(:new).with(from: src, to: dst, width:, height:).and_return(resize)
|
|
69
|
+
expect(resize).to receive(:resize!).and_return(success)
|
|
70
|
+
described_class.resize_image(from: src, to: dst, width:, height:)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "returns the result object from Image::Resize#resize" do
|
|
74
|
+
allow(resize).to receive(:call).and_return(success)
|
|
75
|
+
expect(
|
|
76
|
+
described_class.resize_image(from: src, to: dst, width:, height:)
|
|
77
|
+
).to eq(success)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe ".resize (legacy)" do
|
|
82
|
+
let(:success) { NamePlate::Results::SuccessResult.new(value: {path: "out.png"}) }
|
|
83
|
+
let(:failure) { NamePlate::Results::FailureResult.new(error: {message: "nope"}) }
|
|
84
|
+
|
|
85
|
+
it "returns true when resize_image returns SuccessResult" do
|
|
86
|
+
allow(described_class).to receive(:resize_image).and_return(success)
|
|
87
|
+
expect(described_class.resize_image("in.png", "out.png", 50, 50)).to be(success)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "returns false when resize_image returns FailureResult" do
|
|
91
|
+
allow(described_class).to receive(:resize_image).and_return(failure)
|
|
92
|
+
expect(described_class.resize_image("in.png", "out.png", 50, 50)).to be(failure)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
describe ".path_to_url" do
|
|
97
|
+
it "converts a public filesystem path to a URL path" do
|
|
98
|
+
expect(described_class.path_to_url("public/system/nameplate/1/T/226_95_81/64.png"))
|
|
99
|
+
.to eq("/system/nameplate/1/T/226_95_81/64.png")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nameplate"
|
|
4
|
+
require "pry"
|
|
5
|
+
require "pry-byebug"
|
|
6
|
+
|
|
7
|
+
RSpec.configure do |config|
|
|
8
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
|
9
|
+
config.disable_monkey_patching!
|
|
10
|
+
config.expect_with :rspec do |c|
|
|
11
|
+
c.syntax = :expect
|
|
12
|
+
end
|
|
13
|
+
end
|