rubyless 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,14 @@
1
+ == 0.6.0 2010-07-22
2
+
3
+ * Major enhancements
4
+ * Added 'safe_eval' method.
5
+ * Added 'safe_eval_string' method.
6
+ * Better handling of syntax errors.
7
+ * Support for constants.
8
+
9
+ * Minor enhancements
10
+ * Added safe methods on Time (compare, add/subtract number).
11
+
1
12
  == 0.5.0 2010-05-27
2
13
 
3
14
  * Major enhancements
@@ -22,6 +22,17 @@ RubyLess::SafeClass.safe_method_for( Number,
22
22
  [:% , Number] => Number, [:"-@"] => Number
23
23
  )
24
24
 
25
+ RubyLess::SafeClass.safe_method_for( Time,
26
+ [:==, Time] => Boolean, [:< , Time] => Boolean, [:> , Time] => Boolean,
27
+ [:<=, Time] => Boolean, [:>=, Time] => Boolean,
28
+ [:- , Number] => Time, [:+ , Number] => Time
29
+ )
30
+
25
31
  RubyLess::SafeClass.safe_method_for( String,
26
32
  [:==, String] => Boolean
27
33
  )
34
+
35
+ RubyLess::SafeClass.safe_method_for( NilClass,
36
+ [:==, String] => Boolean,
37
+ [:==, Number] => Boolean
38
+ )
@@ -1,3 +1,3 @@
1
1
  module RubyLess
2
- VERSION = '0.5.0'
2
+ VERSION = '0.6.0'
3
3
  end
@@ -10,8 +10,13 @@ module RubyLess
10
10
  PREFIX_OPERATOR = ['-@']
11
11
 
12
12
  def self.translate(string, helper)
13
- sexp = RubyParser.new.parse(string)
14
- self.new(helper).process(sexp)
13
+ if sexp = RubyParser.new.parse(string)
14
+ self.new(helper).process(sexp)
15
+ elsif string.size == 0
16
+ ''
17
+ else
18
+ raise RubyLess::SyntaxError.new("Syntax error")
19
+ end
15
20
  rescue Racc::ParseError => err
16
21
  raise RubyLess::SyntaxError.new(err.message)
17
22
  end
@@ -38,12 +43,26 @@ module RubyLess
38
43
  # send("process_#{method}", exp)
39
44
  end
40
45
 
46
+ def process_const(exp)
47
+ const_name = exp.pop.to_s
48
+ if opts = @helper.respond_to?(:safe_const_type) ? @helper.safe_const_type(const_name) : nil
49
+ t opts[:method], opts
50
+ else
51
+ raise RubyLess::Error.new("Unknown constant '#{const_name}'.")
52
+ end
53
+ end
54
+
41
55
  def process_and(exp)
42
56
  t "(#{process(exp.shift)} and #{process(exp.shift)})", Boolean
43
57
  end
44
58
 
45
59
  def process_or(exp)
46
- t "(#{process(exp.shift)} or #{process(exp.shift)})", Boolean
60
+ left, right = process(exp.shift), process(exp.shift)
61
+ if left.klass == right.klass
62
+ t "(#{left} or #{right})", :class => right.klass, :nil => right.could_be_nil?
63
+ else
64
+ t "(#{left} or #{right})", Boolean
65
+ end
47
66
  end
48
67
 
49
68
  def process_not(exp)
@@ -109,7 +128,7 @@ module RubyLess
109
128
  def process_vcall(exp)
110
129
  var_name = exp.shift
111
130
  unless opts = get_method([var_name], @helper, false)
112
- raise RubyLess::NoMethodError.new("Unknown variable or method '#{var_name}'.")
131
+ raise RubyLess::Error.new("Unknown variable or method '#{var_name}'.")
113
132
  end
114
133
  method = opts[:method]
115
134
  if args = opts[:prepend_args]
@@ -258,7 +277,8 @@ module RubyLess
258
277
  method = opts[:method]
259
278
  arg_list = args ? args.list : []
260
279
 
261
- if receiver.could_be_nil? && opts != SafeClass.safe_method_type_for(NilClass, signature)
280
+ if receiver.could_be_nil? &&
281
+ !(opts == SafeClass.safe_method_type_for(NilClass, signature) && receiver.cond == [receiver])
262
282
  # Do not add a condition if the method applies on nil
263
283
  cond += receiver.cond
264
284
  elsif receiver.literal && (proc = opts[:pre_processor]) && !arg_list.detect {|a| !a.literal}
@@ -73,125 +73,127 @@ module RubyLess
73
73
  end
74
74
  end
75
75
 
76
- def self.included(base)
77
- base.class_eval do
78
-
79
- # Declare safe methods. By providing
80
- #
81
- # The methods hash has the following format:
82
- # signature => return type
83
- # or
84
- # signature => options
85
- # or
86
- # signature => lambda {|h| ... }
87
- #
88
- # The lambda expression will be called with @helper as argument during compilation.
89
- #
90
- # The signature can be either a single symbol or an array containing the method name and type arguments like:
91
- # [:strftime, Time, String]
92
- #
93
- # If your method accepts variable arguments through a Hash, you should declare it with:
94
- # [:img, String, {:mode => String, :max_size => Number}]
95
- #
96
- # Make sure your literal values are of the right type: +:mode+ and +'mode'+ are not the same here.
97
- #
98
- # If the signature is :defaults, the options defined are used as defaults for the other elements defined in the
99
- # same call.
100
- #
101
- # The return type can be a string with the class name or a class.
102
- #
103
- # Options are:
104
- # :class the return type (class name)
105
- # :nil set this to true if the method could return nil
106
- def self.safe_method(methods_hash)
107
- RubyLess::SafeClass.safe_method_for(self, methods_hash)
108
- end
76
+ # Return a safe type from a column
77
+ def self.safe_method_type_for_column(col, is_property = false)
78
+ opts = {}
79
+ opts[:nil] = col.default.nil?
80
+ if col.number?
81
+ opts[:class] = Number
82
+ elsif col.text?
83
+ opts[:class] = String
84
+ else
85
+ opts[:class] = col.klass
86
+ end
87
+ if is_property
88
+ opts[:method] = "prop['#{col.name.gsub("'",'')}']"
89
+ else
90
+ opts[:method] = col.name
91
+ end
109
92
 
110
- # A safe context is simply a safe method that can return nil in some situations. The rest of the
111
- # syntax is the same as #safe_method. We call it a safe context because it enables syntaxes such
112
- # as: if var = my_context(...) ---> enter context.
113
- def self.safe_context(methods_hash)
114
- methods_hash[:defaults] ||= {}
115
- methods_hash[:defaults][:nil] = true
116
- safe_method(methods_hash)
117
- end
93
+ opts
94
+ end
118
95
 
119
- def self.safe_literal_class(hash)
120
- RubyLess::SafeClass.safe_literal_class(hash)
121
- end
96
+ module ClassMethods
122
97
 
123
- # Declare a safe method to access a list of attributes.
124
- # This method should only be used when the class is linked with a database table and provides
125
- # proper introspection to detect types and the possibility of NULL values.
126
- def self.safe_attribute(*attributes)
127
- attributes.each do |att|
128
- if col = columns_hash[att.to_s]
129
- opts = {}
130
- opts[:nil] = col.default.nil?
131
- if col.number?
132
- opts[:class] = Number
133
- elsif col.text?
134
- opts[:class] = String
135
- else
136
- opts[:class] = col.klass
137
- end
138
- safe_method att.to_sym => opts
139
- else
140
- puts "Warning: could not declare safe_attribute '#{att}' (No column with this name found in class #{self})"
141
- end
98
+ # Declare safe methods. By providing
99
+ #
100
+ # The methods hash has the following format:
101
+ # signature => return type
102
+ # or
103
+ # signature => options
104
+ # or
105
+ # signature => lambda {|h| ... }
106
+ #
107
+ # The lambda expression will be called with @helper as argument during compilation.
108
+ #
109
+ # The signature can be either a single symbol or an array containing the method name and type arguments like:
110
+ # [:strftime, Time, String]
111
+ #
112
+ # If your method accepts variable arguments through a Hash, you should declare it with:
113
+ # [:img, String, {:mode => String, :max_size => Number}]
114
+ #
115
+ # Make sure your literal values are of the right type: +:mode+ and +'mode'+ are not the same here.
116
+ #
117
+ # If the signature is :defaults, the options defined are used as defaults for the other elements defined in the
118
+ # same call.
119
+ #
120
+ # The return type can be a string with the class name or a class.
121
+ #
122
+ # Options are:
123
+ # :class the return type (class name)
124
+ # :nil set this to true if the method could return nil
125
+ def safe_method(methods_hash)
126
+ RubyLess::SafeClass.safe_method_for(self, methods_hash)
127
+ end
128
+
129
+ # A safe context is simply a safe method that can return nil in some situations. The rest of the
130
+ # syntax is the same as #safe_method. We call it a safe context because it enables syntaxes such
131
+ # as: if var = my_context(...) ---> enter context.
132
+ def safe_context(methods_hash)
133
+ methods_hash[:defaults] ||= {}
134
+ methods_hash[:defaults][:nil] = true
135
+ safe_method(methods_hash)
136
+ end
137
+
138
+ def safe_literal_class(hash)
139
+ RubyLess::SafeClass.safe_literal_class(hash)
140
+ end
141
+
142
+ # Declare a safe method to access a list of attributes.
143
+ # This method should only be used when the class is linked with a database table and provides
144
+ # proper introspection to detect types and the possibility of NULL values.
145
+ def safe_attribute(*attributes)
146
+ attributes.each do |att|
147
+ if col = columns_hash[att.to_s]
148
+ safe_method att.to_sym => SafeClass.safe_method_type_for_column(col)
149
+ else
150
+ puts "Warning: could not declare safe_attribute '#{att}' (No column with this name found in class #{self})"
142
151
  end
143
152
  end
153
+ end
144
154
 
145
- # Declare a safe method to access a list of properties.
146
- # This method should only be used in conjunction with the Property gem.
147
- def self.safe_property(*properties)
148
- columns = schema.columns
149
- properties.each do |att|
150
- if col = columns[att.to_s]
151
- opts = {}
152
- opts[:nil] = col.default.nil?
153
- if col.number?
154
- opts[:class] = Number
155
- elsif col.text?
156
- opts[:class] = String
157
- else
158
- opts[:class] = col.klass
159
- end
160
- opts[:method] = "prop['#{att.to_s.gsub("'",'')}']"
161
- safe_method att.to_sym => opts
162
- else
163
- puts "Warning: could not declare safe_property '#{att}' (No property column with this name found in class #{self})"
164
- end
155
+ # Declare a safe method to access a list of properties.
156
+ # This method should only be used in conjunction with the Property gem.
157
+ def safe_property(*properties)
158
+ columns = schema.columns
159
+ properties.each do |att|
160
+ if col = columns[att.to_s]
161
+ safe_method att.to_sym => SafeClass.safe_method_type_for_column(col, true)
162
+ else
163
+ puts "Warning: could not declare safe_property '#{att}' (No property column with this name found in class #{self})"
165
164
  end
166
165
  end
166
+ end
167
167
 
168
+ # Declare a safe method for a given class
169
+ def safe_method_for(klass, signature)
170
+ SafeClass.safe_method_for(klass, signature)
171
+ end
168
172
 
169
- # Declare a safe method for a given class
170
- def self.safe_method_for(klass, signature)
171
- SafeClass.safe_method_for(klass, signature)
172
- end
173
+ # Hash of all safe methods defined for the class.
174
+ def safe_methods
175
+ SafeClass.safe_methods_for(self)
176
+ end
173
177
 
174
- # Hash of all safe methods defined for the class.
175
- def self.safe_methods
176
- SafeClass.safe_methods_for(self)
177
- end
178
+ # Return the type if the given signature corresponds to a safe method for the class.
179
+ def safe_method_type(signature)
180
+ SafeClass.safe_method_type_for(self, signature)
181
+ end
178
182
 
179
- # Return the type if the given signature corresponds to a safe method for the class.
180
- def self.safe_method_type(signature)
181
- SafeClass.safe_method_type_for(self, signature)
182
- end
183
+ # Return true if the class is safe (we can call safe_read on its instances)
184
+ def safe_class?
185
+ true
186
+ end
183
187
 
184
- # Return true if the class is safe (we can call safe_read on its instances)
185
- def self.safe_class?
186
- true
187
- end
188
+ # Use this if you want to disable 'safe_read'. This is useful if you provided
189
+ # a mock as return type signature.
190
+ def disable_safe_read
191
+ undef_method(:safe_read)
192
+ end
193
+ end # ClassMethods
188
194
 
189
- # Use this if you want to disable 'safe_read'. This is useful if you provided
190
- # a mock as return type signature.
191
- def self.disable_safe_read
192
- undef_method(:safe_read)
193
- end
194
- end # base.class_eval
195
+ def self.included(base)
196
+ base.extend ClassMethods
195
197
  end # included
196
198
 
197
199
 
@@ -203,12 +205,25 @@ module RubyLess
203
205
  end
204
206
 
205
207
  # Safe attribute reader used when 'safe_readable?' could not be called because the class
206
- # is not known during compile time. FIXME: Is this used anymore ?
208
+ # is not known during compile time.
209
+ # FIXME: Is this used anymore ?
207
210
  def safe_read(key)
208
211
  return "'#{key}' not readable" unless type = self.class.safe_method_type([key])
209
212
  self.send(type[:method])
210
213
  end
211
214
 
215
+ # Evaluate a RubyLess expression. This is just like 'eval' but with safe method checking and typing.
216
+ def safe_eval(code)
217
+ ruby = RubyLessProcessor.translate(code, self)
218
+ eval(ruby)
219
+ end
220
+
221
+ # Evaluate a RubyLess expression. This is just like 'eval' but with safe method checking and typing.
222
+ def safe_eval_string(code)
223
+ ruby = RubyLess.translate_string(code, self)
224
+ eval(ruby)
225
+ end
226
+
212
227
  private
213
228
  def self.build_signature(key)
214
229
  keys = key.kind_of?(Array) ? key : [key]
data/lib/ruby_less.rb CHANGED
@@ -24,6 +24,12 @@ module RubyLess
24
24
 
25
25
  def self.translate(string, helper)
26
26
  RubyLessProcessor.translate(string, helper)
27
+ rescue Exception => err
28
+ if err.kind_of?(RubyLess::Error)
29
+ raise err
30
+ else
31
+ raise RubyLess::Error.new("Error parsing \"#{string}\": #{err.message.strip}")
32
+ end
27
33
  end
28
34
 
29
35
  def self.translate_string(string, helper)
@@ -32,8 +38,12 @@ module RubyLess
32
38
  else
33
39
  TypedString.new(string.inspect, :class => String, :literal => string)
34
40
  end
35
- rescue => err
36
- raise RubyLess::Error.new("Error parsing string \"#{string}\": #{err.message.strip}")
41
+ rescue Exception => err
42
+ if err.kind_of?(RubyLess::Error)
43
+ raise err
44
+ else
45
+ raise RubyLess::Error.new("Error parsing string \"#{string}\": #{err.message.strip}")
46
+ end
37
47
  end
38
48
  end
39
49
 
data/rubyless.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{rubyless}
8
- s.version = "0.5.0"
8
+ s.version = "0.6.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Gaspard Bucher"]
12
- s.date = %q{2010-05-27}
12
+ s.date = %q{2010-07-22}
13
13
  s.description = %q{RubyLess is an interpreter for "safe ruby". The idea is to transform some "unsafe" ruby code into safe, type checked ruby, eventually rewriting some variables or methods.}
14
14
  s.email = %q{gaspard@teti.ch}
15
15
  s.extra_rdoc_files = [
@@ -41,6 +41,7 @@ Gem::Specification.new do |s|
41
41
  "test/RubyLess/errors.yml",
42
42
  "test/RubyLess/hash.yml",
43
43
  "test/RubyLess/string.yml",
44
+ "test/RubyLess/time.yml",
44
45
  "test/RubyLess_test.rb",
45
46
  "test/mock/active_record_mock.rb",
46
47
  "test/mock/dummy_class.rb",
@@ -1,6 +1,6 @@
1
1
  empty:
2
2
  src: ""
3
- tem: null
3
+ tem: ""
4
4
 
5
5
  numbers:
6
6
  src: "id > 45 and (3 > -id or 3+3)"
@@ -184,4 +184,21 @@ build_finder:
184
184
 
185
185
  methods_on_nil:
186
186
  src: 'dictionary[:foo].blank?'
187
- tem: 'get_dict[:foo].blank?'
187
+ tem: 'get_dict[:foo].blank?'
188
+
189
+ equality_on_nil:
190
+ src: 'dictionary[:foo] == "yo"'
191
+ tem: '(get_dict[:foo]=="yo")'
192
+
193
+ or_same_class:
194
+ src: "vowel_count(dictionary[:bar] || 'aeiou')"
195
+ tem: 'vowel_count((get_dict[:bar] or "aeiou"))'
196
+ res: '5'
197
+
198
+ class:
199
+ src: "@foo.kind_of?(Page)"
200
+ tem: "node.kind_of?(Page)"
201
+
202
+ class_map_as_string:
203
+ src: "@foo.kind_of?(Document)"
204
+ tem: "node.is_like?(\"DOC\")"
@@ -55,3 +55,23 @@ hash_arguments_wrong_type:
55
55
  mixed_array:
56
56
  src: "[3, '4']"
57
57
  tem: 'Mixed Array not supported ([Number,String]).'
58
+
59
+ hash_odd_keys_syntax_error:
60
+ src: '{text}'
61
+ tem: 'Syntax error'
62
+
63
+ syntax_error:
64
+ src: "3 * / * 5"
65
+ tem: "/nterminated string meets end of file/"
66
+
67
+ no_lasagna_in_rubyless:
68
+ src: "foo = system"
69
+ tem: "'lasgn' not available in RubyLess."
70
+
71
+ call_on_class:
72
+ src: "Page.name"
73
+ tem: "unknown method 'name()' for 'Page' of type Class."
74
+
75
+ unknown_constant:
76
+ src: "RubyLess"
77
+ tem: "Unknown constant 'RubyLess'."
@@ -0,0 +1,15 @@
1
+ minus:
2
+ src: "now - 4"
3
+ tem: "(Time.now-4)"
4
+
5
+ plus:
6
+ src: "now + 2* 3600"
7
+ tem: "(Time.now+(2*3600))"
8
+
9
+ compare:
10
+ src: "now == now"
11
+ tem: "(Time.now==Time.now)"
12
+
13
+ compare:
14
+ src: "now > now"
15
+ tem: "(Time.now>Time.now)"
@@ -26,6 +26,8 @@ class RubyLessTest < Test::Unit::TestCase
26
26
 
27
27
  safe_method_for Time, [:strftime, String] => String
28
28
 
29
+ safe_method :now => {:method => 'Time.now', :class => Time}
30
+
29
31
  safe_method :@foo => {:class => Dummy, :method => "node"}
30
32
  safe_method :sub => SubDummy
31
33
  safe_method :str => SubString
@@ -46,12 +48,22 @@ class RubyLessTest < Test::Unit::TestCase
46
48
  # methods on nil
47
49
  safe_method_for Object, :blank? => Boolean
48
50
 
51
+ def safe_const_type(constant)
52
+ if constant == 'Page'
53
+ {:method => 'Page', :class => Class}
54
+ elsif constant =~ /^D/
55
+ {:method => constant[0..2].upcase.inspect, :class => String, :literal => constant}
56
+ else
57
+ nil
58
+ end
59
+ end
60
+
49
61
  # Example to dynamically rewrite method calls during compilation
50
62
  def safe_method_type(signature)
51
63
  unless res = super
52
64
  if signature == ['prepend_test', Number]
53
65
  res ={:class => Number, :prepend_args => RubyLess::TypedString.new('10', :class => Number), :method => 'add'}
54
- elsif res = context[:node_class].safe_method_type(signature)
66
+ elsif context && res = context[:node_class].safe_method_type(signature)
55
67
  # try to execute method in the current var "var.method"
56
68
  res = res.call(self, signature) if res.kind_of?(Proc)
57
69
  res = res.merge(:method => "#{context[:node]}.#{res[:method] || signature[0]}")
@@ -4,6 +4,10 @@ module RubyLess
4
4
  @opts = opts
5
5
  end
6
6
 
7
+ def name
8
+ @opts[:name]
9
+ end
10
+
7
11
  def default
8
12
  @opts[:default]
9
13
  end
@@ -42,10 +46,10 @@ module RubyLess
42
46
 
43
47
  class ActiveRecordMock
44
48
  COLUMNS = {
45
- 'format' => ColumnMock.new(:default => '%d.%m.%Y', :type => :text),
46
- 'age' => ColumnMock.new(:default => 5, :type => :float),
47
- 'friend_id' => ColumnMock.new(:type => :integer),
48
- 'log_at' => ColumnMock.new(:type => :datetime),
49
+ 'format' => ColumnMock.new(:default => '%d.%m.%Y', :type => :text, :name => 'format'),
50
+ 'age' => ColumnMock.new(:default => 5, :type => :float, :name => 'age'),
51
+ 'friend_id' => ColumnMock.new(:type => :integer, :name => 'friend_id'),
52
+ 'log_at' => ColumnMock.new(:type => :datetime, :name => 'log_at'),
49
53
  }
50
54
  def self.columns_hash
51
55
  COLUMNS
@@ -16,7 +16,10 @@ class Dummy < RubyLess::ActiveRecordMock
16
16
  :id => {:class => Number, :method => :zip},
17
17
  :name => String,
18
18
  :foo => :bar,
19
- [:width, {:mode => String, :type => String, 'nice' => Boolean}] => String
19
+ [:width, {:mode => String, :type => String, 'nice' => Boolean}] => String,
20
+ [:kind_of?, Class] => Boolean,
21
+ [:kind_of?, String] => {:method => 'is_like?', :class => Boolean}
22
+
20
23
  safe_context :spouse => 'Dummy',
21
24
  :husband => {:class => 'Dummy', :context => {:clever => 'no'}}
22
25
 
@@ -12,5 +12,32 @@ class SafeClassTest < Test::Unit::TestCase
12
12
  should 'have an associated SignatureHash for safe methods' do
13
13
  assert_kind_of RubyLess::SignatureHash, Dummy.safe_methods
14
14
  end
15
- end
15
+ end # A safe model
16
+
17
+ context 'An instance of a safe model' do
18
+ subject do
19
+ Dummy.new
20
+ end
21
+
22
+ context 'on safe_eval' do
23
+ should 'evaluate RubyLess' do
24
+ assert_equal 'Biscotte', subject.safe_eval("dog_name")
25
+ end
26
+
27
+ should 'raise NoMethodError on missing method' do
28
+ assert_raise(RubyLess::NoMethodError) { subject.safe_eval("bad_method('Bp Oil Spill')") }
29
+ end
30
+ end # on safe_eval
31
+
32
+ context 'on safe_eval_string' do
33
+ should 'evaluate RubyLess as dstring' do
34
+ assert_equal 'my Biscotte', subject.safe_eval_string('my #{dog_name}')
35
+ end
36
+
37
+ should 'raise NoMethodError on missing method' do
38
+ assert_raise(RubyLess::NoMethodError) { subject.safe_eval_string("their \#{bad_method('Bp Oil Spill')}") }
39
+ end
40
+ end # on safe_eval
41
+
42
+ end # An instance of a safe model
16
43
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 5
7
+ - 6
8
8
  - 0
9
- version: 0.5.0
9
+ version: 0.6.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Gaspard Bucher
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-05-27 00:00:00 +02:00
17
+ date: 2010-07-22 00:00:00 +02:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -105,6 +105,7 @@ files:
105
105
  - test/RubyLess/errors.yml
106
106
  - test/RubyLess/hash.yml
107
107
  - test/RubyLess/string.yml
108
+ - test/RubyLess/time.yml
108
109
  - test/RubyLess_test.rb
109
110
  - test/mock/active_record_mock.rb
110
111
  - test/mock/dummy_class.rb