coppertone 0.0.7 → 0.0.12
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 +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
|