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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +122 -0
  4. data/Rakefile +39 -0
  5. data/lib/nameplate/avatar/cache.rb +59 -0
  6. data/lib/nameplate/avatar/generator.rb +237 -0
  7. data/lib/nameplate/avatar/identity.rb +41 -0
  8. data/lib/nameplate/avatar.rb +37 -0
  9. data/lib/nameplate/colors/palette.rb +45 -0
  10. data/lib/nameplate/colors/palettes/custom.rb +20 -0
  11. data/lib/nameplate/colors/palettes/dracula.rb +29 -0
  12. data/lib/nameplate/colors/palettes/google.rb +55 -0
  13. data/lib/nameplate/colors/palettes/iwanthue.rb +234 -0
  14. data/lib/nameplate/colors/palettes/jedi_light.rb +26 -0
  15. data/lib/nameplate/colors/palettes/monokai.rb +27 -0
  16. data/lib/nameplate/colors/palettes/pastel.rb +24 -0
  17. data/lib/nameplate/colors.rb +42 -0
  18. data/lib/nameplate/configuration.rb +109 -0
  19. data/lib/nameplate/fonts/Lato-Light.ttf +0 -0
  20. data/lib/nameplate/fonts/Roboto-Medium +0 -0
  21. data/lib/nameplate/has_avatar.rb +33 -0
  22. data/lib/nameplate/image/resize.rb +111 -0
  23. data/lib/nameplate/results/failure_result.rb +35 -0
  24. data/lib/nameplate/results/success_result.rb +27 -0
  25. data/lib/nameplate/utils/path_helper.rb +18 -0
  26. data/lib/nameplate/version.rb +5 -0
  27. data/lib/nameplate/view_helpers/avatar_helper.rb +60 -0
  28. data/lib/nameplate.rb +57 -0
  29. data/spec/fixtures/in.png +0 -0
  30. data/spec/nameplate/avatar/cache_spec.rb +35 -0
  31. data/spec/nameplate/avatar/generator_spec.rb +108 -0
  32. data/spec/nameplate/avatar/identity_spec.rb +27 -0
  33. data/spec/nameplate/colors_spec.rb +34 -0
  34. data/spec/nameplate/image/resize_spec.rb +96 -0
  35. data/spec/nameplate/utils/path_helper_spec.rb +16 -0
  36. data/spec/nameplate/view_helpers/avatar_helper_spec.rb +38 -0
  37. data/spec/nameplate_spec.rb +102 -0
  38. data/spec/spec_helper.rb +13 -0
  39. metadata +111 -0
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NamePlate
4
+ module Colors
5
+ module Palettes
6
+ class Google < Palette
7
+ GOOGLE_COLORS = [
8
+ [226, 95, 81], # A
9
+ [242, 96, 145], # B
10
+ [187, 101, 202], # C
11
+ [149, 114, 207], # D
12
+ [120, 132, 205], # E
13
+ [91, 149, 249], # F
14
+ [72, 194, 249], # G
15
+ [69, 208, 226], # H
16
+ [72, 182, 172], # I
17
+ [82, 188, 137], # J
18
+ [155, 206, 95], # K
19
+ [212, 227, 74], # L
20
+ [254, 218, 16], # M
21
+ [247, 192, 0], # N
22
+ [255, 168, 0], # O
23
+ [255, 138, 96], # P
24
+ [194, 194, 194], # Q
25
+ [143, 164, 175], # R
26
+ [162, 136, 126], # S
27
+ [163, 163, 163], # T
28
+ [175, 181, 226], # U
29
+ [179, 155, 221], # V
30
+ [194, 194, 194], # W
31
+ [124, 222, 235], # X
32
+ [188, 170, 164], # Y
33
+ [173, 214, 125] # Z
34
+ ].freeze
35
+
36
+ def self.key = :google
37
+
38
+ def initialize
39
+ super(GOOGLE_COLORS)
40
+ end
41
+
42
+ def pick(username)
43
+ char = username[0].upcase
44
+ if /[A-Z]/.match?(char)
45
+ colors[char.ord - 65]
46
+ elsif /[0-9]/.match?(char)
47
+ colors[char.to_i]
48
+ else
49
+ super
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NamePlate
4
+ module Colors
5
+ module Palettes
6
+ class Iwanthue < Palette
7
+ IWANTHUE_COLORS = [
8
+ [198, 125, 40],
9
+ [61, 155, 243],
10
+ [74, 243, 75],
11
+ [238, 89, 166],
12
+ [52, 240, 224],
13
+ [177, 156, 155],
14
+ [240, 120, 145],
15
+ [111, 154, 78],
16
+ [237, 179, 245],
17
+ [237, 101, 95],
18
+ [89, 239, 155],
19
+ [43, 254, 70],
20
+ [163, 212, 245],
21
+ [65, 152, 142],
22
+ [165, 135, 246],
23
+ [181, 166, 38],
24
+ [187, 229, 206],
25
+ [77, 164, 25],
26
+ [179, 246, 101],
27
+ [234, 93, 37],
28
+ [225, 155, 115],
29
+ [142, 140, 188],
30
+ [223, 120, 140],
31
+ [249, 174, 27],
32
+ [244, 117, 225],
33
+ [137, 141, 102],
34
+ [75, 191, 146],
35
+ [188, 239, 142],
36
+ [164, 199, 145],
37
+ [173, 120, 149],
38
+ [59, 195, 89],
39
+ [222, 198, 220],
40
+ [68, 145, 187],
41
+ [236, 204, 179],
42
+ [159, 195, 72],
43
+ [188, 121, 189],
44
+ [166, 160, 85],
45
+ [181, 233, 37],
46
+ [236, 177, 85],
47
+ [121, 147, 160],
48
+ [234, 218, 110],
49
+ [241, 157, 191],
50
+ [62, 200, 234],
51
+ [133, 243, 34],
52
+ [88, 149, 110],
53
+ [59, 228, 248],
54
+ [183, 119, 118],
55
+ [251, 195, 45],
56
+ [113, 196, 122],
57
+ [197, 115, 70],
58
+ [80, 175, 187],
59
+ [103, 231, 238],
60
+ [240, 72, 133],
61
+ [228, 149, 241],
62
+ [180, 188, 159],
63
+ [172, 132, 85],
64
+ [180, 135, 251],
65
+ [236, 194, 58],
66
+ [217, 176, 109],
67
+ [88, 244, 199],
68
+ [186, 157, 239],
69
+ [113, 230, 96],
70
+ [206, 115, 165],
71
+ [244, 178, 163],
72
+ [230, 139, 26],
73
+ [241, 125, 89],
74
+ [83, 160, 66],
75
+ [107, 190, 166],
76
+ [197, 161, 210],
77
+ [198, 203, 245],
78
+ [238, 117, 19],
79
+ [228, 119, 116],
80
+ [131, 156, 41],
81
+ [145, 178, 168],
82
+ [139, 170, 220],
83
+ [233, 95, 125],
84
+ [87, 178, 230],
85
+ [157, 200, 119],
86
+ [237, 140, 76],
87
+ [229, 185, 186],
88
+ [144, 206, 212],
89
+ [236, 209, 158],
90
+ [185, 189, 79],
91
+ [34, 208, 66],
92
+ [84, 238, 129],
93
+ [133, 140, 134],
94
+ [67, 157, 94],
95
+ [168, 179, 25],
96
+ [140, 145, 240],
97
+ [151, 241, 125],
98
+ [67, 162, 107],
99
+ [200, 156, 21],
100
+ [169, 173, 189],
101
+ [226, 116, 189],
102
+ [133, 231, 191],
103
+ [194, 161, 63],
104
+ [241, 77, 99],
105
+ [241, 217, 53],
106
+ [123, 204, 105],
107
+ [210, 201, 119],
108
+ [229, 108, 155],
109
+ [240, 91, 72],
110
+ [187, 115, 210],
111
+ [240, 163, 100],
112
+ [178, 217, 57],
113
+ [179, 135, 116],
114
+ [204, 211, 24],
115
+ [186, 135, 57],
116
+ [223, 176, 135],
117
+ [204, 148, 151],
118
+ [116, 223, 50],
119
+ [95, 195, 46],
120
+ [123, 160, 236],
121
+ [181, 172, 131],
122
+ [142, 220, 202],
123
+ [240, 140, 112],
124
+ [172, 145, 164],
125
+ [228, 124, 45],
126
+ [135, 151, 243],
127
+ [42, 205, 125],
128
+ [192, 233, 116],
129
+ [119, 170, 114],
130
+ [158, 138, 26],
131
+ [73, 190, 183],
132
+ [185, 229, 243],
133
+ [227, 107, 55],
134
+ [196, 205, 202],
135
+ [132, 143, 60],
136
+ [233, 192, 237],
137
+ [62, 150, 220],
138
+ [205, 201, 141],
139
+ [106, 140, 190],
140
+ [161, 131, 205],
141
+ [135, 134, 158],
142
+ [198, 139, 81],
143
+ [115, 171, 32],
144
+ [101, 181, 67],
145
+ [149, 137, 119],
146
+ [37, 142, 183],
147
+ [183, 130, 175],
148
+ [168, 125, 133],
149
+ [124, 142, 87],
150
+ [236, 156, 171],
151
+ [232, 194, 91],
152
+ [219, 200, 69],
153
+ [144, 219, 34],
154
+ [219, 95, 187],
155
+ [145, 154, 217],
156
+ [165, 185, 100],
157
+ [127, 238, 163],
158
+ [224, 178, 198],
159
+ [119, 153, 120],
160
+ [124, 212, 92],
161
+ [172, 161, 105],
162
+ [231, 155, 135],
163
+ [157, 132, 101],
164
+ [122, 185, 146],
165
+ [53, 166, 51],
166
+ [70, 163, 90],
167
+ [150, 190, 213],
168
+ [210, 107, 60],
169
+ [166, 152, 185],
170
+ [159, 194, 159],
171
+ [39, 141, 222],
172
+ [202, 176, 161],
173
+ [95, 140, 229],
174
+ [168, 142, 87],
175
+ [93, 170, 203],
176
+ [159, 142, 54],
177
+ [14, 168, 39],
178
+ [94, 150, 149],
179
+ [187, 206, 136],
180
+ [157, 224, 166],
181
+ [235, 158, 208],
182
+ [109, 232, 216],
183
+ [141, 201, 87],
184
+ [208, 124, 118],
185
+ [142, 125, 214],
186
+ [19, 237, 174],
187
+ [72, 219, 41],
188
+ [234, 102, 111],
189
+ [168, 142, 79],
190
+ [188, 135, 35],
191
+ [95, 155, 143],
192
+ [148, 173, 116],
193
+ [223, 112, 95],
194
+ [228, 128, 236],
195
+ [206, 114, 54],
196
+ [195, 119, 88],
197
+ [235, 140, 94],
198
+ [235, 202, 125],
199
+ [233, 155, 153],
200
+ [214, 214, 238],
201
+ [246, 200, 35],
202
+ [151, 125, 171],
203
+ [132, 145, 172],
204
+ [131, 142, 118],
205
+ [199, 126, 150],
206
+ [61, 162, 123],
207
+ [58, 176, 151],
208
+ [215, 141, 69],
209
+ [225, 154, 220],
210
+ [220, 77, 167],
211
+ [233, 161, 64],
212
+ [130, 221, 137],
213
+ [81, 191, 129],
214
+ [169, 162, 140],
215
+ [174, 177, 222],
216
+ [236, 174, 47],
217
+ [233, 188, 180],
218
+ [69, 222, 172],
219
+ [71, 232, 93],
220
+ [118, 211, 238],
221
+ [157, 224, 83],
222
+ [218, 105, 73],
223
+ [126, 169, 36]
224
+ ].freeze
225
+
226
+ def self.key = :iwanthue
227
+
228
+ def initialize
229
+ super(IWANTHUE_COLORS)
230
+ end
231
+ end
232
+ end
233
+ end
234
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NamePlate
4
+ module Colors
5
+ module Palettes
6
+ # A "light side" Jedi-inspired palette
7
+ class JediLight < Palette
8
+ COLORS = [
9
+ [255, 255, 255], # pure white
10
+ [0, 87, 183], # Jedi blue
11
+ [114, 137, 218], # softer blue
12
+ [0, 204, 255], # cyan/saber glow
13
+ [255, 214, 10], # light yellow/gold
14
+ [173, 216, 230], # pale blue
15
+ [192, 192, 192] # silver
16
+ ].freeze
17
+
18
+ def self.key = :jedi_light
19
+
20
+ def initialize
21
+ super(COLORS)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NamePlate
4
+ module Colors
5
+ module Palettes
6
+ # Inspired by Monokai theme
7
+ class Monokai < Palette
8
+ COLORS = [
9
+ [39, 40, 34], # background
10
+ [248, 248, 242], # foreground
11
+ [249, 38, 114], # pink
12
+ [166, 226, 46], # green
13
+ [253, 151, 31], # orange
14
+ [102, 217, 239], # cyan
15
+ [174, 129, 255], # purple
16
+ [230, 219, 116] # yellow
17
+ ].freeze
18
+
19
+ def self.key = :monokai
20
+
21
+ def initialize
22
+ super(COLORS)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NamePlate
4
+ module Colors
5
+ module Palettes
6
+ # A soft pastel theme
7
+ class Pastel < Palette
8
+ COLORS = [
9
+ [255, 179, 186], # light pink
10
+ [255, 223, 186], # peach
11
+ [255, 255, 186], # light yellow
12
+ [186, 255, 201], # mint
13
+ [186, 225, 255] # baby blue
14
+ ].freeze
15
+
16
+ def self.key = :pastel
17
+
18
+ def initialize
19
+ super(COLORS)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "colors/palette"
4
+
5
+ # Auto-require all palette classes
6
+ Dir[File.join(__dir__ || Dir.pwd, "colors", "palettes", "*.rb")].sort.each { |f| require f }
7
+
8
+ module NamePlate
9
+ module Colors
10
+ # Lazily build registry from all subclasses of Palette
11
+ # @return [Hash{String => NamePlate::Colors::Palette}] The registry of palettes.
12
+ # use NamePlate::Colors.registry to access it.
13
+ def self.registry
14
+ @registry ||= Palettes.constants.map do |const|
15
+ klass = Palettes.const_get(const)
16
+
17
+ next unless klass.is_a?(Class) && klass < Palette
18
+ [klass.key, klass.new]
19
+ end.compact.to_h.freeze
20
+ end
21
+
22
+ def self.for(username)
23
+ palette = registry.fetch(NamePlate.colors_palette) do
24
+ raise ArgumentError, "Unknown palette: #{NamePlate.colors_palette}"
25
+ end
26
+ palette.pick(username)
27
+ end
28
+
29
+ # Validate a custom palette of colors.
30
+ #
31
+ # @param [Array[Integer]] palette The custom palette of colors.
32
+ # @return [Boolean] Whether the custom palette is valid.
33
+ def self.valid_custom_palette?(palette)
34
+ return false if palette.nil?
35
+ return false unless palette.is_a?(Array)
36
+ return false unless palette.all? { |c| c.is_a?(String) && c.match?(/\A#(?:[0-9a-fA-F]{3}){1,2}\z/) }
37
+ return false if palette.size < 2
38
+ return false if palette.size > 20
39
+ true
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NamePlate
4
+ # Configuration settings for NamePlate.
5
+ # These can be set globally via the {NamePlate.configure} block, or
6
+ # on a per-invocation basis via the options hash.
7
+ # @example Set global configuration
8
+ # NamePlate.configure do |config|
9
+ # config.cache_base_path = "public/system/nameplate" # defaults to "tmp/nameplate" if unset
10
+ # config.font = "/path/to/font.ttf" # defaults to {Avatar::FONT_FILE} if unset
11
+ # config.fill_color = [255, 255, 255] # defaults to {Avatar::FILL_COLOR} if unset
12
+ # config.colors_palette = :google # defaults to :google
13
+ # config.custom_palette = [[255, 255, 255], [0, 0, 0]] # required if colors_palette is :custom
14
+ # config.weight = 300 # defaults to 300
15
+ # config.annotate_position = "-0+5" # defaults to "-0+5"
16
+ # config.letters_count = 1 # defaults to 1
17
+ # config.pointsize = 140 # defaults to 140
18
+ # end
19
+ #
20
+ module Configuration
21
+ def cache_base_path
22
+ @cache_base_path
23
+ end
24
+
25
+ def cache_base_path=(v)
26
+ @cache_base_path = v
27
+ end
28
+
29
+ def font
30
+ @font || Avatar::FONT_FILE
31
+ end
32
+
33
+ def font=(v)
34
+ @font = v
35
+ end
36
+
37
+ def fill_color
38
+ @fill_color || Avatar::FILL_COLOR
39
+ end
40
+
41
+ def fill_color=(v)
42
+ @fill_color = v
43
+ end
44
+
45
+ def colors_palette
46
+ @colors_palette ||= :google
47
+ end
48
+
49
+ # Set the color palette to use.
50
+ #
51
+ # @param [Symbol, Array<Array<Integer>>] v The color palette to use.
52
+ # @return [Symbol, Array<Array<Integer>>] The color palette being used.
53
+ def colors_palette=(v)
54
+ sym = v.to_sym
55
+ raise ArgumentError, "Unknown palette: #{v.inspect}" unless Colors.registry.key?(sym)
56
+ @colors_palette = sym
57
+ end
58
+
59
+ def custom_palette
60
+ @custom_palette ||= nil
61
+ end
62
+
63
+ # Set a custom palette of colors to use when colors_palette is :custom
64
+ #
65
+ # @param [Array[Integer]] v The custom palette of colors.
66
+ # @return [Array[Integer]] The custom palette of colors.
67
+ def custom_palette=(v)
68
+ @custom_palette = v
69
+ if @custom_palette.nil? && @colors_palette == :custom
70
+ raise "Missing Custom Palette, please set config.custom_palette if using :custom"
71
+ end
72
+ if Colors.valid_custom_palette?(@custom_palette)
73
+ raise "Invalid Custom Palette, please update config.custom_palette"
74
+ end
75
+ end
76
+
77
+ def weight
78
+ @weight ||= 300
79
+ end
80
+
81
+ def weight=(v)
82
+ @weight = v
83
+ end
84
+
85
+ def annotate_position
86
+ @annotate_position ||= "-0+5"
87
+ end
88
+
89
+ def annotate_position=(v)
90
+ @annotate_position = v
91
+ end
92
+
93
+ def letters_count
94
+ @letters_count ||= 1
95
+ end
96
+
97
+ def letters_count=(v)
98
+ @letters_count = v
99
+ end
100
+
101
+ def pointsize
102
+ @pointsize ||= 140
103
+ end
104
+
105
+ def pointsize=(v)
106
+ @pointsize = v
107
+ end
108
+ end
109
+ end
Binary file
Binary file
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NamePlate
4
+ # Mixin for models that "have an avatar".
5
+ #
6
+ # Example:
7
+ # class User
8
+ # include NamePlate::HasAvatar
9
+ # attr_accessor :name
10
+ # end
11
+ #
12
+ # user = User.new.tap { |u| u.name = "Tony" }
13
+ # user.avatar_path(128)
14
+ # # => "public/system/nameplate/2/T/226_95_81/128.png"
15
+ #
16
+ module HasAvatar
17
+ # Return the filesystem path to the generated avatar
18
+ #
19
+ # @param size [Integer] size in px (default 64)
20
+ # @return [String] path to avatar image
21
+ def avatar_path(size = 64)
22
+ NamePlate::Avatar.generate(username, size)
23
+ end
24
+
25
+ # Return the URL path to the generated avatar
26
+ #
27
+ # @param size [Integer] size in px (default 64)
28
+ # @return [String] URL for avatar image
29
+ def avatar_url(size = 64)
30
+ NamePlate.path_to_url(avatar_path(size))
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mini_magick"
4
+
5
+ module NamePlate
6
+ module Image
7
+ # Resizes images with MiniMagick.
8
+ #
9
+ # Responsibility: take a source image and produce a resized
10
+ # version that fits the requested WxH, centered, with transparent padding.
11
+ #
12
+ # Used by {Avatar::Generator} when a non-fullsize avatar is requested.
13
+ class Resize
14
+ # Initializes a new Resize object.
15
+ #
16
+ # @param [String] from path to the source image file
17
+ # @param [String] to path to the destination image file
18
+ # @param [Integer] width target width in pixels
19
+ # @param [Integer] height target height in pixels
20
+ def initialize(from:, to:, width:, height:)
21
+ @from = from
22
+ @to = to
23
+ @width = width
24
+ @height = height
25
+ end
26
+
27
+ def self.call(from:, to:, width:, height:)
28
+ new(from:, to:, width:, height:).resize!
29
+ end
30
+
31
+ # Entry point to resize an image.
32
+ #
33
+ # @param from [String] path to the source image file
34
+ # @param to [String] path to the destination image file
35
+ # @param width [Integer] target width in pixels
36
+ # @param height [Integer] target height in pixels
37
+ # @return [NamePlate::Results::SuccessResult, NamePlate::Results::FailureResult]
38
+ def resize!
39
+ validate_inputs!
40
+ process_resize
41
+ rescue => e
42
+ failure_result(e, from:, to:, width:, height:)
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :from, :to, :width, :height
48
+
49
+ # Validates the input parameters.
50
+ #
51
+ # @return [void] if valid
52
+ # @raise [ArgumentError] if any parameter is invalid
53
+ def validate_inputs!
54
+ raise ArgumentError, "Source file not found: #{from}" unless File.exist?(from)
55
+ raise ArgumentError, "Width must be positive integer" unless width.is_a?(Integer) && width.positive?
56
+ raise ArgumentError, "Height must be positive integer" unless height.is_a?(Integer) && height.positive?
57
+ raise ArgumentError, "Destination path cannot be empty" if to.to_s.strip.empty?
58
+ end
59
+
60
+ # Processes the image resizing.
61
+ #
62
+ # @return [NamePlate::Results::SuccessResult, NamePlate::Results::FailureResult]
63
+ def process_resize
64
+ image = MiniMagick::Image.open(from)
65
+
66
+ image.combine_options do |c|
67
+ c.background "transparent"
68
+ c.gravity "center"
69
+ c.thumbnail "#{width}x#{height}^"
70
+ c.extent "#{width}x#{height}"
71
+ c.unsharp "2x0.5+0.7+0"
72
+ c.quality 98
73
+ end
74
+
75
+ image.write(to)
76
+ success_result(to)
77
+ end
78
+
79
+ # Construct a SuccessResult with the given path.
80
+ #
81
+ # @param [String] path the path to the resized image file
82
+ #
83
+ # @return [NamePlate::Results::SuccessResult] the success result
84
+ def success_result(path)
85
+ NamePlate::Results::SuccessResult.new(value: {path:})
86
+ end
87
+
88
+ # Construct a FailureResult with the given exception and context.
89
+ #
90
+ # @param [StandardError] exception the raised exception
91
+ # @param [String] from path to the source image file
92
+ # @param [String] to path to the destination image file
93
+ # @param [Integer] width target width in pixels
94
+ # @param [Integer] height target height in pixels
95
+ #
96
+ # @return [NamePlate::Results::FailureResult] the failure result
97
+ def failure_result(exception, from:, to:, width:, height:)
98
+ NamePlate::Results::FailureResult.new(
99
+ error: {
100
+ message: "Image resize failed: #{exception.message}",
101
+ exception: exception,
102
+ from: from,
103
+ to: to,
104
+ width: width,
105
+ height: height
106
+ }
107
+ )
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NamePlate
4
+ module Results
5
+ # Represents a failed operation result.
6
+ #
7
+ # Provides a consistent API for checking failure
8
+ # and accessing error details.
9
+ class FailureResult
10
+ attr_reader :error
11
+
12
+ def initialize(error:)
13
+ @error = error
14
+ end
15
+
16
+ # @return [Boolean]
17
+ def success?
18
+ false
19
+ end
20
+
21
+ # @return [Boolean]
22
+ def failure(argv, stdout, stderr, status)
23
+ NamePlate::Results::FailureResult.new(
24
+ error: {
25
+ message: "Command failed",
26
+ argv: argv,
27
+ stdout: stdout,
28
+ stderr: stderr,
29
+ status: status
30
+ }
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end