c66-copper 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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