malfunction 0.1.0 → 0.1.1
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 +4 -4
- data/README.md +3 -0
- data/lib/malfunction/attribute_error.rb +15 -0
- data/lib/malfunction/attribute_error_collection.rb +7 -0
- data/lib/malfunction/malfunction/attribute_errors.rb +45 -0
- data/lib/malfunction/malfunction/builder.rb +24 -0
- data/lib/malfunction/malfunction/context.rb +39 -0
- data/lib/malfunction/malfunction/core.rb +43 -0
- data/lib/malfunction/malfunction_base.rb +18 -0
- data/lib/malfunction/rspec/custom_matchers/contextualize_as.rb +34 -0
- data/lib/malfunction/rspec/custom_matchers/define_problem.rb +37 -0
- data/lib/malfunction/rspec/custom_matchers/have_default_problem.rb +20 -0
- data/lib/malfunction/rspec/custom_matchers/use_attribute_errors.rb +28 -0
- data/lib/malfunction/rspec/custom_matchers.rb +6 -0
- data/lib/malfunction/spec_helper.rb +3 -0
- data/lib/malfunction/version.rb +1 -1
- data/lib/malfunction.rb +5 -0
- metadata +34 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 291855d70297ba991d40e72253a04dd4fc356ba2a3882d53ba61a9c8919693ec
|
4
|
+
data.tar.gz: 93e6f3406c3c4b25d0a2a0a929ee577472b7e2654b376873de0fffd46e2c179b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7b2d4c278613f1f92798773c47583e20f40a9126351488d0a59b8357fbd6d52152fbf03b9d5db60b90a8c2a12c21279bb71608583402941a9b663ca554a6b251
|
7
|
+
data.tar.gz: 57dd0ba88f4a6d8109fd9c9ff6112ac353aba3c23f10ea5b242075e35b313ab01c31958ee8474b89d5fc38c5cb5be961579cbd55a77de1a125ef845864656025
|
data/README.md
CHANGED
@@ -3,6 +3,9 @@
|
|
3
3
|
An extensible way to encapsulate the variety of BadStuff that happens in Rails applications
|
4
4
|
|
5
5
|
[](https://badge.fury.io/rb/malfunction)
|
6
|
+
[](https://semaphoreci.com/freshly/malfunction)
|
7
|
+
[](https://codeclimate.com/github/Freshly/malfunction/maintainability)
|
8
|
+
[](https://codeclimate.com/github/Freshly/malfunction/test_coverage)
|
6
9
|
|
7
10
|
* [Installation](#installation)
|
8
11
|
* [Usage](#usage)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Malfunction
|
4
|
+
class AttributeError < Spicerack::InputObject
|
5
|
+
argument :attribute_name
|
6
|
+
argument :error_code
|
7
|
+
|
8
|
+
option :message
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
super || other.try(:attribute_name) == attribute_name && other.try(:error_code) == error_code
|
12
|
+
end
|
13
|
+
alias_method :eql?, :==
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Malfunction
|
4
|
+
module Malfunction
|
5
|
+
module AttributeErrors
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
delegate :uses_attribute_errors?, to: :class
|
10
|
+
memoize :attribute_errors
|
11
|
+
end
|
12
|
+
|
13
|
+
class_methods do
|
14
|
+
def uses_attribute_errors?
|
15
|
+
@uses_attribute_errors.present?
|
16
|
+
end
|
17
|
+
|
18
|
+
def inherited(base)
|
19
|
+
base.uses_attribute_errors if uses_attribute_errors?
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def uses_attribute_errors
|
26
|
+
@uses_attribute_errors = true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def attribute_errors?
|
31
|
+
attribute_errors.present?
|
32
|
+
end
|
33
|
+
|
34
|
+
def attribute_errors
|
35
|
+
AttributeErrorCollection.new if uses_attribute_errors?
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_attribute_error(attribute_name, error_code, message = nil)
|
39
|
+
raise ArgumentError, "#{self.class.name} does not use attribute errors" unless uses_attribute_errors?
|
40
|
+
|
41
|
+
attribute_errors << AttributeError.new(attribute_name: attribute_name, error_code: error_code, message: message)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Malfunction
|
4
|
+
module Malfunction
|
5
|
+
module Builder
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
define_callbacks_with_handler :build
|
10
|
+
memoize :build
|
11
|
+
end
|
12
|
+
|
13
|
+
class_methods do
|
14
|
+
def build(*arguments)
|
15
|
+
new(*arguments).build
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def build
|
20
|
+
run_callbacks(:build) { self }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Malfunction
|
4
|
+
module Malfunction
|
5
|
+
module Context
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
delegate :contextualized?, :allow_nil_context?, to: :class
|
10
|
+
attr_reader :context
|
11
|
+
end
|
12
|
+
|
13
|
+
class_methods do
|
14
|
+
attr_reader :contextualized_as
|
15
|
+
|
16
|
+
def contextualized?
|
17
|
+
@contextualized_as.present?
|
18
|
+
end
|
19
|
+
|
20
|
+
def allow_nil_context?
|
21
|
+
contextualized? && @allow_nil_context
|
22
|
+
end
|
23
|
+
|
24
|
+
def inherited(base)
|
25
|
+
base.contextualize(@contextualized_as, allow_nil: allow_nil_context?) if contextualized?
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def contextualize(contextualize_as, allow_nil: false)
|
32
|
+
@contextualized_as = contextualize_as
|
33
|
+
@allow_nil_context = allow_nil
|
34
|
+
alias_method contextualize_as, :context
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Malfunction
|
4
|
+
module Malfunction
|
5
|
+
module Core
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
delegate :problem, to: :class
|
10
|
+
attr_reader :details
|
11
|
+
end
|
12
|
+
|
13
|
+
class_methods do
|
14
|
+
def problem
|
15
|
+
prototype_name.underscore.to_sym
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(context = nil, details: nil)
|
20
|
+
self.context = context
|
21
|
+
self.details = details
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def context=(context)
|
27
|
+
if context.nil?
|
28
|
+
raise ArgumentError, "#{self.class.name} requires context" if contextualized? && !allow_nil_context?
|
29
|
+
else
|
30
|
+
raise ArgumentError, "#{self.class.name} is not contextualized" unless contextualized?
|
31
|
+
end
|
32
|
+
|
33
|
+
@context = context
|
34
|
+
end
|
35
|
+
|
36
|
+
def details=(details)
|
37
|
+
raise ArgumentError, "details is invalid" unless details.nil? || details.respond_to?(:to_hash)
|
38
|
+
|
39
|
+
@details = details.presence&.to_hash || {}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "malfunction/attribute_errors"
|
4
|
+
require_relative "malfunction/context"
|
5
|
+
require_relative "malfunction/core"
|
6
|
+
require_relative "malfunction/builder"
|
7
|
+
|
8
|
+
module Malfunction
|
9
|
+
class MalfunctionBase < Spicerack::RootObject
|
10
|
+
include Conjunction::Junction
|
11
|
+
suffixed_with "Malfunction"
|
12
|
+
|
13
|
+
include Malfunction::AttributeErrors
|
14
|
+
include Malfunction::Context
|
15
|
+
include Malfunction::Core
|
16
|
+
include Malfunction::Builder
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests usage of the default `MalfunctionBase.contextualize`
|
4
|
+
#
|
5
|
+
# class FooMalfunction < Malfunction::Base
|
6
|
+
# contextualize :fringle
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# class BarMalfunction < Malfunction::Base
|
10
|
+
# contextualize :bangle, allow_nil: true
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# RSpec.describe FooMalfunction, type: :malfunction do
|
14
|
+
# it { is_expected.to contextualize_as :fringle }
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# RSpec.describe BarMalfunction, type: :malfunction do
|
18
|
+
# it { is_expected.not_to contextualize_as :bangle, allow_nil: true }
|
19
|
+
# end
|
20
|
+
|
21
|
+
RSpec::Matchers.define :contextualize_as do |contextualized_as, allow_nil: false|
|
22
|
+
match do
|
23
|
+
expect(test_subject.contextualized_as).to eq contextualized_as
|
24
|
+
expect(test_subject.allow_nil_context?).to eq allow_nil
|
25
|
+
end
|
26
|
+
description { "use contextualize as #{contextualized_as} with allow_nil: #{allow_nil}" }
|
27
|
+
failure_message do
|
28
|
+
"expected #{test_subject.name} to contextualize as #{contextualized_as} with allow_nil: #{allow_nil}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_subject
|
32
|
+
subject.is_a?(Module) ? subject : subject.class
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests usage of `MalfunctionBase.problem`
|
4
|
+
#
|
5
|
+
# class FooBarMalfunction < Malfunction::Base
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
# class BazMalfunction < Malfunction::Base
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# class CustomMalfunction < Malfunction::Base
|
12
|
+
# def self.problem
|
13
|
+
# :something_else
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# RSpec.describe FooBarMalfunction, type: :malfunction do
|
18
|
+
# it { is_expected.to define_problem :foo_bar }
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# RSpec.describe BazMalfunction, type: :malfunction do
|
22
|
+
# it { is_expected.to define_problem :baz }
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# RSpec.describe CustomMalfunction, type: :malfunction do
|
26
|
+
# it { is_expected.to define_problem :something_else }
|
27
|
+
# end
|
28
|
+
|
29
|
+
RSpec::Matchers.define :define_problem do |problem|
|
30
|
+
match { expect(test_subject.problem).to eq problem }
|
31
|
+
description { "defines problem #{problem}" }
|
32
|
+
failure_message { "expected #{test_subject.name} to define problem #{problem}" }
|
33
|
+
|
34
|
+
def test_subject
|
35
|
+
subject.is_a?(Module) ? subject : subject.class
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests usage of the default `MalfunctionBase.problem`
|
4
|
+
#
|
5
|
+
# class FooBarMalfunction < Malfunction::Base
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
# RSpec.describe FooBarMalfunction, type: :malfunction do
|
9
|
+
# it { is_expected.to have_default_problem }
|
10
|
+
# end
|
11
|
+
|
12
|
+
RSpec::Matchers.define :have_default_problem do
|
13
|
+
match { expect(test_subject.problem).to eq test_subject.prototype_name.underscore.to_sym }
|
14
|
+
description { "have default problem" }
|
15
|
+
failure_message { "expected #{test_subject.name} to have default problem" }
|
16
|
+
|
17
|
+
def test_subject
|
18
|
+
subject.is_a?(Module) ? subject : subject.class
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# RSpec matcher that tests usage of the default `MalfunctionBase.uses_attribute_errors`
|
4
|
+
#
|
5
|
+
# class FooMalfunction < Malfunction::Base
|
6
|
+
# uses_attribute_errors
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# class BarMalfunction < Malfunction::Base
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# RSpec.describe FooMalfunction, type: :malfunction do
|
13
|
+
# it { is_expected.to use_attribute_errors }
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# RSpec.describe BarMalfunction, type: :malfunction do
|
17
|
+
# it { is_expected.not_to use_attribute_errors }
|
18
|
+
# end
|
19
|
+
|
20
|
+
RSpec::Matchers.define :use_attribute_errors do
|
21
|
+
match { expect(test_subject.uses_attribute_errors?).to eq true }
|
22
|
+
description { "use attribute errors" }
|
23
|
+
failure_message { "expected #{test_subject.name} to use attribute errors" }
|
24
|
+
|
25
|
+
def test_subject
|
26
|
+
subject.is_a?(Module) ? subject : subject.class
|
27
|
+
end
|
28
|
+
end
|
data/lib/malfunction/version.rb
CHANGED
data/lib/malfunction.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: malfunction
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Garside
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-01-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -30,7 +30,7 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.
|
33
|
+
version: 0.20.4
|
34
34
|
- - "<"
|
35
35
|
- !ruby/object:Gem::Version
|
36
36
|
version: '1.0'
|
@@ -40,7 +40,7 @@ dependencies:
|
|
40
40
|
requirements:
|
41
41
|
- - ">="
|
42
42
|
- !ruby/object:Gem::Version
|
43
|
-
version: 0.
|
43
|
+
version: 0.20.4
|
44
44
|
- - "<"
|
45
45
|
- !ruby/object:Gem::Version
|
46
46
|
version: '1.0'
|
@@ -58,20 +58,6 @@ dependencies:
|
|
58
58
|
- - "~>"
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: 2.0.1
|
61
|
-
- !ruby/object:Gem::Dependency
|
62
|
-
name: faker
|
63
|
-
requirement: !ruby/object:Gem::Requirement
|
64
|
-
requirements:
|
65
|
-
- - "~>"
|
66
|
-
- !ruby/object:Gem::Version
|
67
|
-
version: '1.8'
|
68
|
-
type: :development
|
69
|
-
prerelease: false
|
70
|
-
version_requirements: !ruby/object:Gem::Requirement
|
71
|
-
requirements:
|
72
|
-
- - "~>"
|
73
|
-
- !ruby/object:Gem::Version
|
74
|
-
version: '1.8'
|
75
61
|
- !ruby/object:Gem::Dependency
|
76
62
|
name: pry-byebug
|
77
63
|
requirement: !ruby/object:Gem::Requirement
|
@@ -101,54 +87,54 @@ dependencies:
|
|
101
87
|
- !ruby/object:Gem::Version
|
102
88
|
version: '10.0'
|
103
89
|
- !ruby/object:Gem::Dependency
|
104
|
-
name:
|
90
|
+
name: simplecov
|
105
91
|
requirement: !ruby/object:Gem::Requirement
|
106
92
|
requirements:
|
107
93
|
- - "~>"
|
108
94
|
- !ruby/object:Gem::Version
|
109
|
-
version: '
|
95
|
+
version: '0.16'
|
110
96
|
type: :development
|
111
97
|
prerelease: false
|
112
98
|
version_requirements: !ruby/object:Gem::Requirement
|
113
99
|
requirements:
|
114
100
|
- - "~>"
|
115
101
|
- !ruby/object:Gem::Version
|
116
|
-
version: '
|
102
|
+
version: '0.16'
|
117
103
|
- !ruby/object:Gem::Dependency
|
118
|
-
name:
|
104
|
+
name: timecop
|
119
105
|
requirement: !ruby/object:Gem::Requirement
|
120
106
|
requirements:
|
121
|
-
- - "
|
107
|
+
- - ">="
|
122
108
|
- !ruby/object:Gem::Version
|
123
|
-
version:
|
109
|
+
version: 0.9.1
|
124
110
|
type: :development
|
125
111
|
prerelease: false
|
126
112
|
version_requirements: !ruby/object:Gem::Requirement
|
127
113
|
requirements:
|
128
|
-
- - "
|
114
|
+
- - ">="
|
129
115
|
- !ruby/object:Gem::Version
|
130
|
-
version:
|
116
|
+
version: 0.9.1
|
131
117
|
- !ruby/object:Gem::Dependency
|
132
|
-
name:
|
118
|
+
name: shoulda-matchers
|
133
119
|
requirement: !ruby/object:Gem::Requirement
|
134
120
|
requirements:
|
135
|
-
- -
|
121
|
+
- - '='
|
136
122
|
- !ruby/object:Gem::Version
|
137
|
-
version: 0.
|
123
|
+
version: 4.0.1
|
138
124
|
type: :development
|
139
125
|
prerelease: false
|
140
126
|
version_requirements: !ruby/object:Gem::Requirement
|
141
127
|
requirements:
|
142
|
-
- -
|
128
|
+
- - '='
|
143
129
|
- !ruby/object:Gem::Version
|
144
|
-
version: 0.
|
130
|
+
version: 4.0.1
|
145
131
|
- !ruby/object:Gem::Dependency
|
146
132
|
name: rspice
|
147
133
|
requirement: !ruby/object:Gem::Requirement
|
148
134
|
requirements:
|
149
135
|
- - ">="
|
150
136
|
- !ruby/object:Gem::Version
|
151
|
-
version: 0.
|
137
|
+
version: 0.20.4
|
152
138
|
- - "<"
|
153
139
|
- !ruby/object:Gem::Version
|
154
140
|
version: '1.0'
|
@@ -158,7 +144,7 @@ dependencies:
|
|
158
144
|
requirements:
|
159
145
|
- - ">="
|
160
146
|
- !ruby/object:Gem::Version
|
161
|
-
version: 0.
|
147
|
+
version: 0.20.4
|
162
148
|
- - "<"
|
163
149
|
- !ruby/object:Gem::Version
|
164
150
|
version: '1.0'
|
@@ -168,7 +154,7 @@ dependencies:
|
|
168
154
|
requirements:
|
169
155
|
- - ">="
|
170
156
|
- !ruby/object:Gem::Version
|
171
|
-
version: 0.
|
157
|
+
version: 0.20.4
|
172
158
|
- - "<"
|
173
159
|
- !ruby/object:Gem::Version
|
174
160
|
version: '1.0'
|
@@ -178,24 +164,10 @@ dependencies:
|
|
178
164
|
requirements:
|
179
165
|
- - ">="
|
180
166
|
- !ruby/object:Gem::Version
|
181
|
-
version: 0.
|
167
|
+
version: 0.20.4
|
182
168
|
- - "<"
|
183
169
|
- !ruby/object:Gem::Version
|
184
170
|
version: '1.0'
|
185
|
-
- !ruby/object:Gem::Dependency
|
186
|
-
name: shoulda-matchers
|
187
|
-
requirement: !ruby/object:Gem::Requirement
|
188
|
-
requirements:
|
189
|
-
- - '='
|
190
|
-
- !ruby/object:Gem::Version
|
191
|
-
version: 4.0.1
|
192
|
-
type: :development
|
193
|
-
prerelease: false
|
194
|
-
version_requirements: !ruby/object:Gem::Requirement
|
195
|
-
requirements:
|
196
|
-
- - '='
|
197
|
-
- !ruby/object:Gem::Version
|
198
|
-
version: 4.0.1
|
199
171
|
description: An extensible way to encapsulate the variety of BadStuff that happens
|
200
172
|
in Rails applications
|
201
173
|
email:
|
@@ -207,6 +179,19 @@ files:
|
|
207
179
|
- LICENSE.txt
|
208
180
|
- README.md
|
209
181
|
- lib/malfunction.rb
|
182
|
+
- lib/malfunction/attribute_error.rb
|
183
|
+
- lib/malfunction/attribute_error_collection.rb
|
184
|
+
- lib/malfunction/malfunction/attribute_errors.rb
|
185
|
+
- lib/malfunction/malfunction/builder.rb
|
186
|
+
- lib/malfunction/malfunction/context.rb
|
187
|
+
- lib/malfunction/malfunction/core.rb
|
188
|
+
- lib/malfunction/malfunction_base.rb
|
189
|
+
- lib/malfunction/rspec/custom_matchers.rb
|
190
|
+
- lib/malfunction/rspec/custom_matchers/contextualize_as.rb
|
191
|
+
- lib/malfunction/rspec/custom_matchers/define_problem.rb
|
192
|
+
- lib/malfunction/rspec/custom_matchers/have_default_problem.rb
|
193
|
+
- lib/malfunction/rspec/custom_matchers/use_attribute_errors.rb
|
194
|
+
- lib/malfunction/spec_helper.rb
|
210
195
|
- lib/malfunction/version.rb
|
211
196
|
homepage: https://github.com/Freshly/malfunction
|
212
197
|
licenses:
|