coppertone 0.0.7 → 0.0.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.circleci/config.yml +79 -0
- data/.codeclimate.yml +28 -0
- data/.github/dependabot.yml +7 -0
- data/.rubocop.yml +47 -6
- data/.travis.yml +2 -6
- data/Gemfile +12 -2
- data/LICENSE +21 -201
- data/Rakefile +12 -6
- data/coppertone.gemspec +9 -9
- data/lib/coppertone/class_builder.rb +2 -0
- data/lib/coppertone/directive.rb +6 -1
- data/lib/coppertone/domain_spec.rb +5 -1
- data/lib/coppertone/error.rb +5 -0
- data/lib/coppertone/ip_address_wrapper.rb +5 -1
- data/lib/coppertone/macro_context.rb +11 -4
- data/lib/coppertone/macro_string.rb +4 -2
- data/lib/coppertone/macro_string/macro_expand.rb +6 -2
- data/lib/coppertone/macro_string/macro_literal.rb +1 -0
- data/lib/coppertone/macro_string/macro_parser.rb +8 -4
- data/lib/coppertone/macro_string/macro_static_expand.rb +3 -1
- data/lib/coppertone/mechanism.rb +2 -0
- data/lib/coppertone/mechanism/all.rb +1 -0
- data/lib/coppertone/mechanism/cidr_parser.rb +3 -3
- data/lib/coppertone/mechanism/domain_spec_mechanism.rb +10 -3
- data/lib/coppertone/mechanism/domain_spec_optional.rb +1 -0
- data/lib/coppertone/mechanism/domain_spec_required.rb +1 -0
- data/lib/coppertone/mechanism/domain_spec_with_dual_cidr.rb +8 -1
- data/lib/coppertone/mechanism/include_matcher.rb +3 -0
- data/lib/coppertone/mechanism/ip_mechanism.rb +20 -10
- data/lib/coppertone/mechanism/mx.rb +1 -0
- data/lib/coppertone/modifier.rb +3 -1
- data/lib/coppertone/modifier/base.rb +7 -2
- data/lib/coppertone/modifier/exp.rb +6 -2
- data/lib/coppertone/modifier/redirect.rb +1 -0
- data/lib/coppertone/modifier/unknown.rb +1 -0
- data/lib/coppertone/null_macro_context.rb +1 -1
- data/lib/coppertone/qualifier.rb +1 -0
- data/lib/coppertone/record.rb +6 -2
- data/lib/coppertone/record_evaluator.rb +5 -0
- data/lib/coppertone/record_finder.rb +2 -2
- data/lib/coppertone/record_term_parser.rb +9 -5
- data/lib/coppertone/redirect_record_finder.rb +3 -1
- data/lib/coppertone/request.rb +26 -23
- data/lib/coppertone/request_context.rb +1 -0
- data/lib/coppertone/request_count_limiter.rb +2 -0
- data/lib/coppertone/result.rb +2 -1
- data/lib/coppertone/sender_identity.rb +2 -1
- data/lib/coppertone/term.rb +1 -0
- data/lib/coppertone/terms_parser.rb +3 -1
- data/lib/coppertone/utils/domain_utils.rb +21 -8
- data/lib/coppertone/utils/ip_in_domain_checker.rb +1 -1
- data/lib/coppertone/utils/validated_domain_finder.rb +2 -1
- data/lib/coppertone/version.rb +1 -1
- data/spec/domain_spec_spec.rb +1 -1
- data/spec/macro_string/macro_static_expand_spec.rb +2 -2
- data/spec/mechanism/ip4_spec.rb +3 -0
- data/spec/mechanism/ip6_spec.rb +3 -0
- data/spec/null_macro_context_spec.rb +1 -1
- data/spec/open_spf/Initial_processing_spec.rb +0 -2
- data/spec/open_spf/Semantics_of_exp_and_other_modifiers_spec.rb +0 -2
- data/spec/record_spec.rb +1 -1
- data/spec/spec_helper.rb +14 -3
- data/spec/term_spec.rb +1 -1
- data/spec/utils/domain_utils_spec.rb +10 -2
- metadata +34 -32
data/Rakefile
CHANGED
@@ -23,15 +23,16 @@ desc 'Parse OpenSPF tests'
|
|
23
23
|
|
24
24
|
def spec_file_name(doc, output_path)
|
25
25
|
description = doc['description']
|
26
|
-
|
27
|
-
|
28
|
-
|
26
|
+
file_prefix = description.gsub(/[^\w\s_-]+/, '')
|
27
|
+
.gsub(/(^|\b\s)\s+($|\s?\b)/, '\\1\\2')
|
28
|
+
.gsub(/\s+/, '_')
|
29
|
+
file_name = "#{file_prefix}_spec.rb"
|
29
30
|
File.join(output_path, file_name)
|
30
31
|
end
|
31
32
|
|
32
33
|
INDENT_STRING = ' '.freeze
|
33
34
|
def indented_string(num_indents = 1)
|
34
|
-
Array.new(num_indents) { INDENT_STRING }.join
|
35
|
+
Array.new(num_indents) { INDENT_STRING }.join
|
35
36
|
end
|
36
37
|
|
37
38
|
def puts_prefixed_string(f, s, indent = 0)
|
@@ -57,21 +58,25 @@ end
|
|
57
58
|
|
58
59
|
def escape_quote(val)
|
59
60
|
return unless val
|
61
|
+
|
60
62
|
val.gsub(/'/) { |_s| "\\'" }
|
61
63
|
end
|
62
64
|
|
63
65
|
def clean_description(description)
|
64
66
|
return unless description
|
67
|
+
|
65
68
|
description.tr("\n", ' ').delete("'")
|
66
69
|
end
|
67
70
|
|
68
71
|
def as_array(val)
|
69
72
|
return [] unless val
|
73
|
+
|
70
74
|
val.is_a?(Array) ? val : [val]
|
71
75
|
end
|
72
76
|
|
73
77
|
def write_comment(f, comment, indent)
|
74
78
|
return unless comment
|
79
|
+
|
75
80
|
comment.lines do |comment_line|
|
76
81
|
puts_prefixed_string(f, "# #{comment_line}", indent)
|
77
82
|
end
|
@@ -85,10 +90,11 @@ def write_result(f, host, mailfrom, helo, indent)
|
|
85
90
|
end
|
86
91
|
|
87
92
|
def write_expects(f, results, explanation, indent)
|
88
|
-
results_array = "[#{results.map { |r|
|
93
|
+
results_array = "[#{results.map { |r| ":#{r}" }.join(',')}]"
|
89
94
|
code_expect = "expect(#{results_array}).to include(result.code)"
|
90
95
|
puts_prefixed_string(f, code_expect, indent)
|
91
96
|
return unless explanation
|
97
|
+
|
92
98
|
exp_expect = "expect(result.explanation).to eq('#{explanation}')"
|
93
99
|
puts_prefixed_string(f, exp_expect, indent)
|
94
100
|
end
|
@@ -116,7 +122,7 @@ def write_spec(f, spec, indent = 1)
|
|
116
122
|
end
|
117
123
|
|
118
124
|
def write_doc(doc, output_path)
|
119
|
-
open(spec_file_name(doc, output_path), 'w') do |f|
|
125
|
+
File.open(spec_file_name(doc, output_path), 'w') do |f|
|
120
126
|
puts_prefixed_string(f, "require 'spec_helper'")
|
121
127
|
empty_line(f)
|
122
128
|
description = doc['description']
|
data/coppertone.gemspec
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
3
|
require 'coppertone/version'
|
5
4
|
|
@@ -7,22 +6,23 @@ Gem::Specification.new do |spec|
|
|
7
6
|
spec.name = 'coppertone'
|
8
7
|
spec.version = Coppertone::VERSION
|
9
8
|
spec.authors = ['Peter M. Goldstein']
|
10
|
-
spec.email = ['peter
|
9
|
+
spec.email = ['peter@valimail.com']
|
11
10
|
spec.summary = 'A Sender Policy Framework (SPF) toolkit'
|
12
11
|
spec.description = 'Coppertone includes tools for parsing SPF DNS records, evaluating the result of SPF checks for received emails, and creating appropriate email headers from SPF results.'
|
13
|
-
spec.homepage = 'https://github.com/
|
14
|
-
spec.license = '
|
12
|
+
spec.homepage = 'https://github.com/ValiMail/coppertone'
|
13
|
+
spec.license = 'MIT'
|
15
14
|
spec.files = `git ls-files -z`.split("\x0")
|
16
15
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
16
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
17
|
spec.require_paths = ['lib']
|
18
|
+
spec.required_ruby_version = '>= 2.7'
|
19
19
|
|
20
|
+
spec.add_runtime_dependency 'activesupport', '>= 3.0'
|
21
|
+
spec.add_runtime_dependency 'addressable'
|
20
22
|
spec.add_runtime_dependency 'dns_adapter'
|
21
23
|
spec.add_runtime_dependency 'i18n'
|
22
|
-
spec.add_runtime_dependency 'addressable'
|
23
|
-
spec.add_runtime_dependency 'activesupport', '>= 3.0'
|
24
24
|
spec.add_development_dependency 'bundler'
|
25
|
-
spec.add_development_dependency '
|
25
|
+
spec.add_development_dependency 'flay'
|
26
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
26
27
|
spec.add_development_dependency 'rspec', '>= 3.0'
|
27
|
-
spec.add_development_dependency 'rubocop'
|
28
28
|
end
|
data/lib/coppertone/directive.rb
CHANGED
@@ -7,6 +7,7 @@ module Coppertone
|
|
7
7
|
# SPF specification (see section 4.6.1).
|
8
8
|
class Directive
|
9
9
|
attr_reader :qualifier, :mechanism
|
10
|
+
|
10
11
|
delegate :context_dependent?, :dns_lookup_term?,
|
11
12
|
:includes_ptr?, to: :mechanism
|
12
13
|
|
@@ -25,6 +26,7 @@ module Coppertone
|
|
25
26
|
|
26
27
|
def target_domain
|
27
28
|
raise NeedsContextError unless dns_lookup_term?
|
29
|
+
|
28
30
|
mechanism.target_domain
|
29
31
|
end
|
30
32
|
|
@@ -37,17 +39,20 @@ module Coppertone
|
|
37
39
|
qualifier.default? ? mechanism_s : "#{qualifier}#{mechanism_s}"
|
38
40
|
end
|
39
41
|
|
40
|
-
DIRECTIVE_REGEXP = /\A([
|
42
|
+
DIRECTIVE_REGEXP = /\A([+\-~?]?)([a-zA-Z0-9]*)((:?)\S*)\z/.freeze
|
41
43
|
def self.matching_term(text)
|
42
44
|
return nil if text.include?('=')
|
45
|
+
|
43
46
|
matches = DIRECTIVE_REGEXP.match(text)
|
44
47
|
return nil unless matches
|
48
|
+
|
45
49
|
qualifier_txt = matches[1]
|
46
50
|
mechanism_type = matches[2].downcase
|
47
51
|
attributes = matches[3]
|
48
52
|
qualifier = Qualifier.find_by_text(qualifier_txt)
|
49
53
|
mechanism = Mechanism.build(mechanism_type, attributes)
|
50
54
|
return nil unless qualifier && mechanism
|
55
|
+
|
51
56
|
new(qualifier, mechanism)
|
52
57
|
end
|
53
58
|
end
|
@@ -15,10 +15,11 @@ module Coppertone
|
|
15
15
|
|
16
16
|
def validate_domain_spec_restrictions
|
17
17
|
return if only_allowed_macros? && ends_in_allowed_term?
|
18
|
+
|
18
19
|
raise Coppertone::DomainSpecParsingError
|
19
20
|
end
|
20
21
|
|
21
|
-
EXP_ONLY_MACRO_LETTERS = %w
|
22
|
+
EXP_ONLY_MACRO_LETTERS = %w[c r t].freeze
|
22
23
|
def only_allowed_macros?
|
23
24
|
@macros.select { |m| m.is_a?(Coppertone::MacroString::MacroExpand) }
|
24
25
|
.none? { |m| EXP_ONLY_MACRO_LETTERS.include?(m.macro_letter) }
|
@@ -29,6 +30,7 @@ module Coppertone
|
|
29
30
|
return true unless lm
|
30
31
|
return false if lm.is_a?(Coppertone::MacroString::MacroStaticExpand)
|
31
32
|
return true if lm.is_a?(Coppertone::MacroString::MacroExpand)
|
33
|
+
|
32
34
|
ends_with_top_label?
|
33
35
|
end
|
34
36
|
|
@@ -37,8 +39,10 @@ module Coppertone
|
|
37
39
|
ends_with = ends_with[0..-2] if ends_with[-1] == '.'
|
38
40
|
_, match, tail = ends_with.rpartition('.')
|
39
41
|
return false if match.blank?
|
42
|
+
|
40
43
|
hostname = Coppertone::Utils::DomainUtils.valid_tld?(tail)
|
41
44
|
return false unless hostname
|
45
|
+
|
42
46
|
true
|
43
47
|
end
|
44
48
|
end
|
data/lib/coppertone/error.rb
CHANGED
@@ -4,11 +4,13 @@ module Coppertone
|
|
4
4
|
|
5
5
|
# Error classes mapping to the SPF result codes
|
6
6
|
class TemperrorError < Coppertone::Error; end
|
7
|
+
|
7
8
|
class PermerrorError < Coppertone::Error; end
|
8
9
|
|
9
10
|
# Errors occurring when the string representation of a MacroString
|
10
11
|
# or DomainSpec does not obey the syntax requirements.
|
11
12
|
class MacroStringParsingError < Coppertone::PermerrorError; end
|
13
|
+
|
12
14
|
class DomainSpecParsingError < MacroStringParsingError; end
|
13
15
|
|
14
16
|
# Occurs when an SPF record cannot be parsed.
|
@@ -42,8 +44,11 @@ module Coppertone
|
|
42
44
|
|
43
45
|
# Errors generated when certain spec-defined limits are exceeded.
|
44
46
|
class LimitExceededError < Coppertone::PermerrorError; end
|
47
|
+
|
45
48
|
class TermLimitExceededError < PermerrorError; end
|
49
|
+
|
46
50
|
class VoidLimitExceededError < PermerrorError; end
|
51
|
+
|
47
52
|
class MXLimitExceededError < PermerrorError; end
|
48
53
|
|
49
54
|
# Raised when context is required to evaluate a value, but
|
@@ -10,9 +10,11 @@ module Coppertone
|
|
10
10
|
# will fail if passed an address with a non-trivial network prefix
|
11
11
|
class IPAddressWrapper
|
12
12
|
attr_reader :string_representation, :ip
|
13
|
+
|
13
14
|
def initialize(s)
|
14
15
|
@ip = self.class.parse(s)
|
15
16
|
raise ArgumentError unless @ip
|
17
|
+
|
16
18
|
@string_representation = s
|
17
19
|
end
|
18
20
|
|
@@ -26,6 +28,7 @@ module Coppertone
|
|
26
28
|
def self.parse(s)
|
27
29
|
return nil unless s
|
28
30
|
return nil if s.index('/')
|
31
|
+
|
29
32
|
ip_addr = IPAddr.new(s)
|
30
33
|
normalize_ip(ip_addr)
|
31
34
|
rescue IP_PARSE_ERROR
|
@@ -33,7 +36,8 @@ module Coppertone
|
|
33
36
|
end
|
34
37
|
|
35
38
|
def self.normalize_ip(parsed_ip)
|
36
|
-
return parsed_ip unless parsed_ip
|
39
|
+
return parsed_ip unless parsed_ip&.ipv6?
|
40
|
+
|
37
41
|
parsed_ip.ipv4_mapped? ? parsed_ip.native : parsed_ip
|
38
42
|
end
|
39
43
|
|
@@ -7,7 +7,7 @@ module Coppertone
|
|
7
7
|
# A context used to evaluate MacroStrings. Responds to all of the
|
8
8
|
# macro letter directives except 'p'.
|
9
9
|
class MacroContext
|
10
|
-
attr_reader :domain, :ip_address_wrapper, :sender_identity, :helo_domain
|
10
|
+
attr_reader :domain, :ip_address_wrapper, :sender_identity, :helo_domain, :hostname
|
11
11
|
|
12
12
|
delegate :s, :l, :o, to: :sender_identity
|
13
13
|
alias d domain
|
@@ -16,7 +16,6 @@ module Coppertone
|
|
16
16
|
to: :ip_address_wrapper
|
17
17
|
alias h helo_domain
|
18
18
|
|
19
|
-
attr_reader :hostname
|
20
19
|
def initialize(domain, ip_as_s, sender, helo_domain = 'unknown',
|
21
20
|
options = {})
|
22
21
|
@ip_address_wrapper = IPAddressWrapper.new(ip_as_s)
|
@@ -37,10 +36,18 @@ module Coppertone
|
|
37
36
|
end
|
38
37
|
|
39
38
|
RESERVED_REGEXP = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
|
40
|
-
|
39
|
+
|
40
|
+
def escape(string)
|
41
|
+
encoding = string.encoding
|
42
|
+
string.b.gsub(RESERVED_REGEXP) do |m|
|
43
|
+
"%#{m.unpack('H2' * m.bytesize).join('%').upcase}"
|
44
|
+
end.force_encoding(encoding)
|
45
|
+
end
|
46
|
+
|
47
|
+
%w[s l o d i v h c r t].each do |m|
|
41
48
|
define_method(m.upcase) do
|
42
49
|
unencoded = send(m)
|
43
|
-
unencoded ?
|
50
|
+
unencoded ? escape(unencoded) : nil
|
44
51
|
end
|
45
52
|
end
|
46
53
|
|
@@ -9,6 +9,7 @@ module Coppertone
|
|
9
9
|
# context for interpolation.
|
10
10
|
class MacroString
|
11
11
|
attr_reader :macro_text
|
12
|
+
|
12
13
|
def initialize(macro_text)
|
13
14
|
@macro_text = macro_text
|
14
15
|
macros
|
@@ -19,11 +20,11 @@ module Coppertone
|
|
19
20
|
end
|
20
21
|
|
21
22
|
def expand(context, request = nil)
|
22
|
-
macros.map { |m| m.expand(context, request) }.join
|
23
|
+
macros.map { |m| m.expand(context, request) }.join
|
23
24
|
end
|
24
25
|
|
25
26
|
def to_s
|
26
|
-
macros.map(&:to_s).join
|
27
|
+
macros.map(&:to_s).join
|
27
28
|
end
|
28
29
|
|
29
30
|
def context_dependent?
|
@@ -39,6 +40,7 @@ module Coppertone
|
|
39
40
|
|
40
41
|
def ==(other)
|
41
42
|
return false unless other.instance_of? self.class
|
43
|
+
|
42
44
|
macro_text == other.macro_text
|
43
45
|
end
|
44
46
|
alias eql? ==
|
@@ -9,10 +9,10 @@ module Coppertone
|
|
9
9
|
# grammer defined in section 7.1.
|
10
10
|
class MacroExpand
|
11
11
|
MACRO_LETTER_CHAR_SET = '[slodiphcrtvSLODIPHCRTV]'.freeze
|
12
|
-
PTR_MACRO_CHAR_SET = %w
|
12
|
+
PTR_MACRO_CHAR_SET = %w[p P].freeze
|
13
13
|
DELIMITER_CHAR_SET = '[\.\-\+\,\/\_\=]'.freeze
|
14
14
|
VALID_BODY_REGEXP =
|
15
|
-
/\A(#{MACRO_LETTER_CHAR_SET})(\d*)(r?)(#{DELIMITER_CHAR_SET}*)\z
|
15
|
+
/\A(#{MACRO_LETTER_CHAR_SET})(\d*)(r?)(#{DELIMITER_CHAR_SET}*)\z/.freeze
|
16
16
|
|
17
17
|
attr_reader :macro_letter, :digit_transformers, :reverse,
|
18
18
|
:delimiter_regexp
|
@@ -20,6 +20,7 @@ module Coppertone
|
|
20
20
|
def initialize(s)
|
21
21
|
matches = VALID_BODY_REGEXP.match(s)
|
22
22
|
raise Coppertone::MacroStringParsingError if matches.nil?
|
23
|
+
|
23
24
|
@macro_letter = matches[1]
|
24
25
|
initialize_digit_transformers(matches[2])
|
25
26
|
@reverse = (matches[3] == 'r')
|
@@ -29,6 +30,7 @@ module Coppertone
|
|
29
30
|
|
30
31
|
def initialize_digit_transformers(raw_value)
|
31
32
|
return unless raw_value
|
33
|
+
|
32
34
|
@digit_transformers = raw_value.to_i unless raw_value.empty?
|
33
35
|
return unless @digit_transformers
|
34
36
|
raise Coppertone::MacroStringParsingError if @digit_transformers.zero?
|
@@ -44,6 +46,7 @@ module Coppertone
|
|
44
46
|
Coppertone::Utils::ValidatedDomainFinder
|
45
47
|
.new(context, request, false).find(context.d)
|
46
48
|
return 'unknown' unless ptr
|
49
|
+
|
47
50
|
@macro_letter == 'P' ? ::Addressable::URI.encode_component(ptr) : ptr
|
48
51
|
end
|
49
52
|
|
@@ -64,6 +67,7 @@ module Coppertone
|
|
64
67
|
|
65
68
|
def ==(other)
|
66
69
|
return false unless other.instance_of? self.class
|
70
|
+
|
67
71
|
to_s == other.to_s
|
68
72
|
end
|
69
73
|
|
@@ -9,6 +9,7 @@ module Coppertone
|
|
9
9
|
# in the context of a particular SPF check.
|
10
10
|
class MacroParser
|
11
11
|
attr_reader :macros
|
12
|
+
|
12
13
|
def initialize(s)
|
13
14
|
@s = s.dup
|
14
15
|
@macros = []
|
@@ -31,20 +32,23 @@ module Coppertone
|
|
31
32
|
|
32
33
|
def parse_contextual_interpolated_macro
|
33
34
|
raise MacroStringParsingError unless @s[1] == '{'
|
35
|
+
|
34
36
|
closing_index = @s.index('}')
|
35
37
|
raise MacroStringParsingError unless closing_index
|
38
|
+
|
36
39
|
interpolated_body = @s[2, closing_index - 2]
|
37
40
|
@macros << MacroExpand.new(interpolated_body)
|
38
|
-
@s = @s[(closing_index + 1)
|
41
|
+
@s = @s[(closing_index + 1)..]
|
39
42
|
end
|
40
43
|
|
41
|
-
SIMPLE_MACRO_LETTERS = %w
|
44
|
+
SIMPLE_MACRO_LETTERS = %w[% _ -].freeze
|
42
45
|
def parse_interpolated_macro
|
43
46
|
raise MacroStringParsingError if @s.length == 1
|
47
|
+
|
44
48
|
macro_code = @s[0, 2]
|
45
49
|
if MacroStaticExpand.exists_for?(macro_code)
|
46
50
|
@macros << MacroStaticExpand.macro_for(macro_code)
|
47
|
-
@s = @s[2
|
51
|
+
@s = @s[2..]
|
48
52
|
else
|
49
53
|
parse_contextual_interpolated_macro
|
50
54
|
end
|
@@ -54,7 +58,7 @@ module Coppertone
|
|
54
58
|
new_idx = @s.index('%')
|
55
59
|
new_idx ||= @s.length
|
56
60
|
@macros << MacroLiteral.new(@s[0, new_idx])
|
57
|
-
@s = @s[new_idx
|
61
|
+
@s = @s[new_idx..]
|
58
62
|
new_idx
|
59
63
|
end
|
60
64
|
end
|
@@ -28,14 +28,16 @@ module Coppertone
|
|
28
28
|
# Replaces '%-' in a macro string
|
29
29
|
URL_ENCODED_SPACE_MACRO = new('%-'.freeze, '%20'.freeze)
|
30
30
|
|
31
|
-
SIMPLE_INTERPOLATED_MACRO_LETTERS = %w
|
31
|
+
SIMPLE_INTERPOLATED_MACRO_LETTERS = %w[% _ -].freeze
|
32
32
|
def self.exists_for?(x)
|
33
33
|
return false unless x && (x.length == 2) && (x[0] == '%')
|
34
|
+
|
34
35
|
SIMPLE_INTERPOLATED_MACRO_LETTERS.include?(x[1])
|
35
36
|
end
|
36
37
|
|
37
38
|
def self.macro_for(x)
|
38
39
|
raise Coppertone::MacroStringParsingError unless exists_for?(x)
|
40
|
+
|
39
41
|
case x[1]
|
40
42
|
when '%'
|
41
43
|
PERCENT_MACRO
|
data/lib/coppertone/mechanism.rb
CHANGED
@@ -18,6 +18,7 @@ module Coppertone
|
|
18
18
|
|
19
19
|
def self.register(klass)
|
20
20
|
raise ArgumentError unless klass < self
|
21
|
+
|
21
22
|
class_builder.register(klass.label, klass)
|
22
23
|
end
|
23
24
|
|
@@ -26,6 +27,7 @@ module Coppertone
|
|
26
27
|
end
|
27
28
|
|
28
29
|
attr_reader :arguments
|
30
|
+
|
29
31
|
def initialize(arguments)
|
30
32
|
@arguments = arguments
|
31
33
|
end
|