c66-copper 0.0.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 +7 -0
- data/README.md +5 -0
- data/bin/console +14 -0
- data/bin/copper +96 -0
- data/bin/setup +8 -0
- data/lib/copper/action.rb +9 -0
- data/lib/copper/attribute.rb +30 -0
- data/lib/copper/attribute_params.rb +9 -0
- data/lib/copper/attributes.rb +21 -0
- data/lib/copper/attributes_right_associated.rb +11 -0
- data/lib/copper/boolean.rb +9 -0
- data/lib/copper/comparison.rb +58 -0
- data/lib/copper/compop.rb +8 -0
- data/lib/copper/copper.rb +32 -0
- data/lib/copper/copper_node.rb +16 -0
- data/lib/copper/data_types/array.rb +61 -0
- data/lib/copper/data_types/data_type.rb +54 -0
- data/lib/copper/data_types/ip_addr.rb +82 -0
- data/lib/copper/data_types/range.rb +17 -0
- data/lib/copper/data_types/semver.rb +45 -0
- data/lib/copper/data_types/string.rb +23 -0
- data/lib/copper/error.rb +5 -0
- data/lib/copper/expression.rb +8 -0
- data/lib/copper/expression_utils.rb +13 -0
- data/lib/copper/functions/fetch.rb +27 -0
- data/lib/copper/functions/ip_address.rb +18 -0
- data/lib/copper/grammar/copper.treetop +161 -0
- data/lib/copper/identifier.rb +9 -0
- data/lib/copper/loader.rb +13 -0
- data/lib/copper/logic.rb +14 -0
- data/lib/copper/logic_op.rb +7 -0
- data/lib/copper/logic_right_associated.rb +16 -0
- data/lib/copper/number.rb +10 -0
- data/lib/copper/param.rb +13 -0
- data/lib/copper/param_right_associated.rb +10 -0
- data/lib/copper/parser.rb +42 -0
- data/lib/copper/range.rb +15 -0
- data/lib/copper/root.rb +13 -0
- data/lib/copper/rule_definition.rb +21 -0
- data/lib/copper/set.rb +11 -0
- data/lib/copper/single_var_definition.rb +20 -0
- data/lib/copper/string.rb +17 -0
- data/lib/copper/var_definition.rb +11 -0
- data/lib/copper/variable.rb +9 -0
- data/lib/copper/variable_identifier.rb +15 -0
- data/lib/copper/version.rb +5 -0
- data/lib/copper.rb +5 -0
- metadata +247 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bb48cc101f266912993eabc1c9836ca74d693afb
|
4
|
+
data.tar.gz: b13bfa681af1d729ca5185ddff419940e5921713
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b1137fb18f6e4f9ce9772056fad87741df3ecf9a47f9b61110038021bfc2b1551840644ae957e28b44b2d7fe060f543f9c35f650954806d7decdfd100412fb9c
|
7
|
+
data.tar.gz: a69ea15d41f510779d435b9eb74bfad4752396d551dcbd5ee513c7b40ab0fd9263d02d35a0d745e02b195a655035c5c74a28c8b73a8d85e30e248a91ef67375f
|
data/README.md
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "copper"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/copper
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'thor'
|
3
|
+
require 'yaml'
|
4
|
+
require 'colorize'
|
5
|
+
require_relative '../lib/copper'
|
6
|
+
|
7
|
+
module Copper
|
8
|
+
class CopperCLI < Thor
|
9
|
+
package_name "copper"
|
10
|
+
|
11
|
+
desc "version", "Show Copper version"
|
12
|
+
def version
|
13
|
+
say "#{::Copper::APP_NAME} #{::Copper::VERSION}\n#{::Copper::COPYRIGHT_MESSAGE}"
|
14
|
+
end
|
15
|
+
|
16
|
+
desc "update", "Update Copper to the latest version"
|
17
|
+
option :version, type: :string, desc: "Force a specific version"
|
18
|
+
def update
|
19
|
+
say "Updating Copper..."
|
20
|
+
unless options[:version]
|
21
|
+
say `gem install copper --no-ri --no-rdoc`
|
22
|
+
else
|
23
|
+
say `gem install copper -v #{options[:version]} --no-ri --no-rdoc`
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "check", "Runs the given rules against a file"
|
28
|
+
option :rules, type: :string, desc: "Rules in ccop file format"
|
29
|
+
option :file, type: :string, desc: "Configuration file"
|
30
|
+
option :format, type: :string, enum: ['yaml'], default: 'yaml'
|
31
|
+
option :debug, type: :boolean, default: false
|
32
|
+
option :'parserdebug', type: :boolean, default: false
|
33
|
+
def check
|
34
|
+
$debug = options[:debug] || false
|
35
|
+
$parser_debug = options[:'parserdebug'] || false
|
36
|
+
require 'byebug' if $debug
|
37
|
+
|
38
|
+
rule_file = options[:rules]
|
39
|
+
unless File.exists?(rule_file)
|
40
|
+
puts "Rule file #{rule_file} not found".red
|
41
|
+
exit(1)
|
42
|
+
end
|
43
|
+
content_file = options[:file]
|
44
|
+
unless File.exists?(content_file)
|
45
|
+
puts "Config file #{content_file} not found".red
|
46
|
+
exit(1)
|
47
|
+
end
|
48
|
+
|
49
|
+
rules = File.read(rule_file)
|
50
|
+
file = ""
|
51
|
+
raise ::NotImplementedError if options[:format] != 'yaml'
|
52
|
+
# load the yaml file and split them into separate yamls
|
53
|
+
|
54
|
+
content = File.read(content_file)
|
55
|
+
content.split('---').each_with_index do |part, idx|
|
56
|
+
puts "Validating part #{idx}"
|
57
|
+
file = YAML::load(part)
|
58
|
+
validate(rules, file)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
no_commands {
|
63
|
+
def validate(rules, file)
|
64
|
+
ccop = ::Copper::Copper.new(rules, { context: file })
|
65
|
+
results = ccop.execute
|
66
|
+
|
67
|
+
if results.nil?
|
68
|
+
puts "Aborting"
|
69
|
+
exit
|
70
|
+
end
|
71
|
+
|
72
|
+
results.each do |rule|
|
73
|
+
action = rule[:action]
|
74
|
+
outcome = rule[:outcome]
|
75
|
+
if !outcome
|
76
|
+
if action == :warn
|
77
|
+
outcome_text = 'WARN'.yellow
|
78
|
+
else
|
79
|
+
outcome_text = 'FAIL'.red
|
80
|
+
end
|
81
|
+
else
|
82
|
+
outcome_text = 'PASS'.green
|
83
|
+
end
|
84
|
+
puts "\t#{rule[:name]} - #{outcome_text}"
|
85
|
+
end
|
86
|
+
rescue ::Copper::ParseError => exc
|
87
|
+
puts "Syntax error: #{exc.message} at line #{ccop.parser.failure_line}, column: #{ccop.parser.failure_column}"
|
88
|
+
rescue ::Copper::RuntimeError => exc
|
89
|
+
puts "Runtime error: #{exc.message}"
|
90
|
+
end
|
91
|
+
}
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
CopperCLI.start
|
96
|
+
end
|
data/bin/setup
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Copper
|
2
|
+
|
3
|
+
class Attribute < CopperNode
|
4
|
+
|
5
|
+
# attribute takes in a parent as it always depends on another node
|
6
|
+
# like "abc".count here count is the attribute and string (abc) is the parent
|
7
|
+
# parent is a PORO
|
8
|
+
def value(parent, vars = {})
|
9
|
+
raise "invalid use of attribute" if parent.nil? # this shouldn't happen as parser will not let .count to be called without a parent
|
10
|
+
|
11
|
+
# 0 is the name of the attribute
|
12
|
+
attribute = elements[0].value
|
13
|
+
# 1 is the params and is optional
|
14
|
+
params = elements[1].value(vars) unless elements[1].nil?
|
15
|
+
|
16
|
+
# now we should call the func based on the name of the attribute with params on parent
|
17
|
+
# here we first convert the PORO to a DataType to make the calls safe
|
18
|
+
dt_obj = factory(parent)
|
19
|
+
if dt_obj.respond_to?(attribute)
|
20
|
+
if params.nil?
|
21
|
+
return dt_obj.send(attribute)
|
22
|
+
else
|
23
|
+
return dt_obj.send(attribute, *params)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
raise ParseError, "#{attribute} is not a valid attribute for #{dt_obj.class.name}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Copper
|
2
|
+
|
3
|
+
class Attributes < CopperNode
|
4
|
+
|
5
|
+
# parent is the object attributes work on
|
6
|
+
# like "abc".map(xxx).count : abc (string) is parent for gsub and array from map is the parent for count
|
7
|
+
def value(parent, vars = {})
|
8
|
+
# this has all the attributes as an array. they will be [attribute, attributes]
|
9
|
+
# attributes will then be [attribute and attributes] and so on
|
10
|
+
|
11
|
+
current_obj = elements[0].value(parent, vars)
|
12
|
+
# no right association, so we are the last attribute
|
13
|
+
return current_obj if elements[1].nil?
|
14
|
+
|
15
|
+
# has right association, which means we need to get the value of the current attribute and pass into the
|
16
|
+
# next one
|
17
|
+
return elements[1].value(current_obj, vars)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Copper
|
2
|
+
class Comparison < CopperNode
|
3
|
+
|
4
|
+
def value(vars = {})
|
5
|
+
lhs = elements[0].value(vars)
|
6
|
+
comp_op = elements[1].value(vars)
|
7
|
+
rhs = elements[2].value(vars)
|
8
|
+
|
9
|
+
raise ParseError, "cannot compare nil" if rhs.nil? || lhs.nil?
|
10
|
+
|
11
|
+
begin
|
12
|
+
case comp_op
|
13
|
+
when '='
|
14
|
+
return equality(lhs, rhs)
|
15
|
+
when '=='
|
16
|
+
return equality(lhs, rhs)
|
17
|
+
when '>'
|
18
|
+
return lhs > rhs
|
19
|
+
when '<'
|
20
|
+
return lhs < rhs
|
21
|
+
when '<='
|
22
|
+
return lhs <= rhs
|
23
|
+
when '>='
|
24
|
+
return lhs > rhs
|
25
|
+
when '!='
|
26
|
+
return lhs != rhs
|
27
|
+
when 'in'
|
28
|
+
# this depends on the types
|
29
|
+
rhs_class = ::Copper::DataTypes::DataType.get_class(rhs.class.name)
|
30
|
+
rhs_obj = rhs_class.new(rhs)
|
31
|
+
if rhs_obj.respond_to?(:in)
|
32
|
+
return rhs_obj.in(lhs)
|
33
|
+
else
|
34
|
+
raise ::Copper::RuntimeError, "in is not a valid comparision on #{rhs_class.name}"
|
35
|
+
end
|
36
|
+
|
37
|
+
when '->'
|
38
|
+
# this is a special case to pipe lhs to console
|
39
|
+
console(lhs)
|
40
|
+
end
|
41
|
+
rescue NoMethodError => exc
|
42
|
+
raise ParseError, "comparison error: #{exc.message}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def equality(lhs, rhs)
|
49
|
+
if lhs.is_a?(Array) && rhs.is_a?(Array)
|
50
|
+
return lhs.sort == rhs.sort
|
51
|
+
else
|
52
|
+
return lhs == rhs
|
53
|
+
end
|
54
|
+
rescue ArgumentError => exc
|
55
|
+
raise ParseError, "equality comparison failed: #{exc.message}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Copper
|
2
|
+
class Copper
|
3
|
+
def initialize(content, vars = {})
|
4
|
+
@content = content
|
5
|
+
@vars = vars
|
6
|
+
|
7
|
+
# add the resevered ones
|
8
|
+
@vars[:variables] = ::Copper::DataTypes::DataType::RESEVERED_TYPES
|
9
|
+
@parser = Parser.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute(options = {})
|
13
|
+
root_node = @parser.parse(@content)
|
14
|
+
result = root_node.value(@vars) unless root_node.nil?
|
15
|
+
puts result if $debug
|
16
|
+
|
17
|
+
return result
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
self.execute(@content, @vars)
|
22
|
+
return true
|
23
|
+
rescue CopperError
|
24
|
+
return false
|
25
|
+
end
|
26
|
+
|
27
|
+
def parser
|
28
|
+
@parser.cc_parser
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Copper
|
2
|
+
# Copper parser node with helper methods for cleaning.
|
3
|
+
class CopperNode < Treetop::Runtime::SyntaxNode
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def console(text)
|
8
|
+
puts "[DEBUG] #{self.class.name} ==> #{text} (#{text.class.name})" if $debug
|
9
|
+
end
|
10
|
+
|
11
|
+
def factory(poro)
|
12
|
+
return ::Copper::DataTypes::DataType.factory(poro)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Copper
|
2
|
+
module DataTypes
|
3
|
+
class Array < DataType
|
4
|
+
|
5
|
+
def count
|
6
|
+
return @value.size
|
7
|
+
end
|
8
|
+
|
9
|
+
def first
|
10
|
+
raise RuntimeError, "cannot call first on an empty array" if @value.empty?
|
11
|
+
return @value.first
|
12
|
+
end
|
13
|
+
|
14
|
+
def last
|
15
|
+
raise RuntimeError, "cannot call last on an empty array" if @value.empty?
|
16
|
+
return @value.last
|
17
|
+
end
|
18
|
+
|
19
|
+
def in(value)
|
20
|
+
@value.include?(value)
|
21
|
+
end
|
22
|
+
|
23
|
+
# get item at index
|
24
|
+
def at(index)
|
25
|
+
if index >= @value.size
|
26
|
+
raise RuntimeError, "index #{index} out of bound [0..#{@result.size - 1}]"
|
27
|
+
else
|
28
|
+
return @value[index]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# map the items into the given class
|
33
|
+
def as(clazz)
|
34
|
+
found_class = ::Copper::DataTypes::DataType.get_class(clazz)
|
35
|
+
return @value.map { |x| found_class.new(x).value }
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract(regex, index)
|
39
|
+
result = []
|
40
|
+
@value.each do |v|
|
41
|
+
raise RuntimeError, "#{v} is not a String" unless v.is_a?(::String)
|
42
|
+
found = v.match(regex)
|
43
|
+
result << nil if found.nil?
|
44
|
+
result << nil if found[index].nil?
|
45
|
+
result << found[index]
|
46
|
+
end
|
47
|
+
|
48
|
+
return result
|
49
|
+
end
|
50
|
+
|
51
|
+
def unique
|
52
|
+
@value.uniq
|
53
|
+
end
|
54
|
+
|
55
|
+
def contains(item)
|
56
|
+
@value.include?(item)
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Copper
|
2
|
+
module DataTypes
|
3
|
+
class DataType
|
4
|
+
|
5
|
+
CLASS_MAP = {
|
6
|
+
"String" => "::Copper::DataTypes::String",
|
7
|
+
"Array" => "::Copper::DataTypes::Array",
|
8
|
+
"Semantic::Version" => "::Copper::DataTypes::Semver",
|
9
|
+
"Semver" => "::Copper::DataTypes::Semver",
|
10
|
+
"Range" => "::Copper::DataTypes::Range",
|
11
|
+
"IPAddress" => "::Copper::DataTypes::IPAddress",
|
12
|
+
"IPAddress::IPv4" => "::Copper::DataTypes::IPAddress",
|
13
|
+
"IPAddress::IPv6" => "::Copper::DataTypes::IPAddress"
|
14
|
+
}
|
15
|
+
|
16
|
+
RESEVERED_TYPES = {
|
17
|
+
semver: "Semver",
|
18
|
+
array: "Array",
|
19
|
+
string: "String",
|
20
|
+
range: "Range",
|
21
|
+
ipaddr: "IPAddress"
|
22
|
+
}
|
23
|
+
|
24
|
+
def initialize(value)
|
25
|
+
@value = value
|
26
|
+
end
|
27
|
+
|
28
|
+
def value
|
29
|
+
@value
|
30
|
+
end
|
31
|
+
|
32
|
+
def as(clazz)
|
33
|
+
found_class = ::Copper::DataTypes::DataType.get_class(clazz)
|
34
|
+
return found_class.new(@value).value
|
35
|
+
end
|
36
|
+
|
37
|
+
# returns a DataType based on the given PORO
|
38
|
+
# this is to control the attribute exposure, better error handling, etc
|
39
|
+
def self.factory(poro)
|
40
|
+
poro_class = poro.class.name
|
41
|
+
|
42
|
+
clazz = self.get_class(poro_class)
|
43
|
+
return clazz.new(poro)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.get_class(class_name)
|
47
|
+
raise RuntimeError, "unknown return value #{class_name}" unless ::Copper::DataTypes::DataType::CLASS_MAP.has_key?(class_name)
|
48
|
+
return Module.const_get(::Copper::DataTypes::DataType::CLASS_MAP[class_name])
|
49
|
+
rescue NameError => exc
|
50
|
+
raise ::Copper::RuntimeError, "invalid return type #{class_name}"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
|
3
|
+
module Copper
|
4
|
+
module DataTypes
|
5
|
+
|
6
|
+
class IPAddress
|
7
|
+
def initialize(value)
|
8
|
+
if value.is_a? ::String
|
9
|
+
@value = ::IPAddress.parse(value)
|
10
|
+
elsif value.is_a?(::IPAddress::IPv4)
|
11
|
+
@value = value
|
12
|
+
elsif value.is_a?(::IPAddress::IPv6)
|
13
|
+
@value = value
|
14
|
+
else
|
15
|
+
raise ::Copper::RuntimeError, "cannot convert #{value} to IPAddress"
|
16
|
+
end
|
17
|
+
rescue ArgumentError => exc
|
18
|
+
raise ::Copper::RuntimeError, exc.message
|
19
|
+
end
|
20
|
+
|
21
|
+
def first
|
22
|
+
@value.first
|
23
|
+
end
|
24
|
+
|
25
|
+
def last
|
26
|
+
@value.last
|
27
|
+
end
|
28
|
+
|
29
|
+
def in(value)
|
30
|
+
raise ::Copper::RuntimeError, "#{value} is not an IP address" unless value.is_a?(::IPAddress)
|
31
|
+
(@value.first.to_i <= value.to_i) && (@value.last.to_i >= value.to_i)
|
32
|
+
end
|
33
|
+
|
34
|
+
def full_address
|
35
|
+
@value.to_string
|
36
|
+
end
|
37
|
+
|
38
|
+
def address
|
39
|
+
@value.address
|
40
|
+
end
|
41
|
+
|
42
|
+
def netmask
|
43
|
+
@value.netmask
|
44
|
+
end
|
45
|
+
|
46
|
+
def octets
|
47
|
+
@value.octets
|
48
|
+
end
|
49
|
+
|
50
|
+
def prefix
|
51
|
+
@value.prefix.to_i
|
52
|
+
end
|
53
|
+
|
54
|
+
def is_network
|
55
|
+
@value.network?
|
56
|
+
end
|
57
|
+
|
58
|
+
def is_loopback
|
59
|
+
@value.loopback?
|
60
|
+
end
|
61
|
+
|
62
|
+
def is_multicast
|
63
|
+
@value.multicast?
|
64
|
+
end
|
65
|
+
|
66
|
+
def is_class_a
|
67
|
+
@value.a?
|
68
|
+
end
|
69
|
+
|
70
|
+
def is_class_b
|
71
|
+
@value.b?
|
72
|
+
end
|
73
|
+
|
74
|
+
def is_class_c
|
75
|
+
@value.c?
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'semantic'
|
2
|
+
|
3
|
+
module Copper
|
4
|
+
module DataTypes
|
5
|
+
class Semver < DataType
|
6
|
+
|
7
|
+
def initialize(value)
|
8
|
+
if value.is_a? ::String
|
9
|
+
@value = ::Semantic::Version.new(value)
|
10
|
+
elsif value.is_a? ::Semantic::Version
|
11
|
+
@value = ::Semantic::Version.new(value.to_s)
|
12
|
+
else
|
13
|
+
raise ::Copper::RuntimeError, "cannot convert #{value} to Semver"
|
14
|
+
end
|
15
|
+
rescue ArgumentError => exc
|
16
|
+
raise ::Copper::RuntimeError, exc.message
|
17
|
+
end
|
18
|
+
|
19
|
+
def major
|
20
|
+
@value.major
|
21
|
+
end
|
22
|
+
|
23
|
+
def minor
|
24
|
+
@value.minor
|
25
|
+
end
|
26
|
+
|
27
|
+
def patch
|
28
|
+
@value.patch
|
29
|
+
end
|
30
|
+
|
31
|
+
def build
|
32
|
+
@value.build
|
33
|
+
end
|
34
|
+
|
35
|
+
def pre
|
36
|
+
@value.pre
|
37
|
+
end
|
38
|
+
|
39
|
+
def satisfies(condition)
|
40
|
+
@value.satisfies?(condition)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Copper
|
2
|
+
module DataTypes
|
3
|
+
class String < DataType
|
4
|
+
|
5
|
+
def count
|
6
|
+
return @value.size
|
7
|
+
end
|
8
|
+
|
9
|
+
def gsub(pattern, replacement)
|
10
|
+
return @value.gsub(pattern, replacement)
|
11
|
+
end
|
12
|
+
|
13
|
+
def at(index)
|
14
|
+
if index >= @value.size
|
15
|
+
raise ParseError, "index #{index} out of bound [0..#{@result.size - 1}]"
|
16
|
+
else
|
17
|
+
return @value[index]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/copper/error.rb
ADDED