Runtime assertions for Ruby
literal.fun
ruby
1# frozen_string_literal: true
2
3Example = Literal::Object
4
5test "positional params are required by default" do
6 example = Class.new(Example) do
7 prop :example, String, :positional
8 end
9
10 assert_raises(ArgumentError) { example.new }
11 refute_raises { example.new("Hello") }
12end
13
14test "keyword params are required by default" do
15 example = Class.new(Example) do
16 prop :example, String
17 end
18
19 assert_raises(ArgumentError) { example.new }
20 refute_raises { example.new(example: "Hello") }
21end
22
23test "nilable positional params are optional" do
24 example = Class.new(Example) do
25 prop :example, _Nilable(String), :positional
26 end
27
28 refute_raises { example.new }
29 refute_raises { example.new("Hello") }
30end
31
32test "nilable keyword params are optional" do
33 example = Class.new(Example) do
34 prop :example, _Nilable(String)
35 end
36
37 refute_raises { example.new }
38 refute_raises { example.new(example: "Hello") }
39end
40
41test "prop? accepts a description" do
42 example = Class.new(Example) do
43 prop? :example, String, description: "An optional example"
44 end
45
46 assert_equal example.literal_properties[:example].description, "An optional example"
47end
48
49test "positional splats are optional" do
50 example = Class.new(Example) do
51 prop :example, _Array(String), :*
52 end
53
54 refute_raises { example.new }
55 refute_raises { example.new("Hello") }
56 refute_raises { example.new("Hello", "World") }
57 refute example.literal_properties[:example].required? { "Expected example to not be required" }
58end
59
60test "keyword splats are optional" do
61 example = Class.new(Example) do
62 prop :example, _Hash(Symbol, String), :**
63 end
64
65 refute_raises { example.new }
66 refute_raises { example.new(example: "Hello") }
67 refute_raises { example.new(example: "Hello", world: "World") }
68 refute example.literal_properties[:example].required? { "Expected example to not be required" }
69end
70
71test "block params are required by default" do
72 example = Class.new(Example) do
73 prop :example, Proc, :&
74 end
75
76 assert_raises(Literal::TypeError) { example.new }
77 refute_raises { example.new { "Hello" } }
78end
79
80test "nilable block params are optional" do
81 example = Class.new(Example) do
82 prop :example, _Nilable(Proc), :&
83 end
84
85 refute_raises { example.new }
86 refute_raises { example.new { "Hello" } }
87end
88
89class Person
90 extend Literal::Properties
91
92 prop :name, String, :positional, reader: :public
93 prop :age, Integer, reader: :public
94end
95
96class Random
97 extend Literal::Properties
98 prop :begin, Integer, :positional, reader: :public
99end
100
101class WithDefaultBlock
102 extend Literal::Properties
103 prop :block, Proc, :&, reader: :public, default: -> { proc { "Hello" } }
104end
105
106class WithContextualDefault
107 extend Literal::Properties
108 prop :hello, String, reader: :private, default: "Hello"
109 prop :world, String, reader: :private, default: "World"
110 prop :combined, String, reader: :public, default: -> { "#{hello} #{world}" }
111end
112
113class WithNilableType
114 extend Literal::Properties
115 prop :name, Literal::Types::NilableType.new(String), :positional
116end
117
118class Empty
119 extend Literal::Properties
120end
121
122test "empty initializer" do
123 refute_raises { Empty.new }
124end
125
126test do
127 person = Person.new("John", age: 30)
128
129 assert_equal person.name, "John"
130 assert_equal person.age, 30
131end
132
133test "initializer type check" do
134 error = assert_raises(Literal::TypeError) { Person.new(1, age: "Joel") }
135
136 assert_equal error.message, <<~ERROR
137 Type mismatch
138
139 #{Person}#initialize (from #{error.backtrace[1]})
140 name
141 Expected: String
142 Actual (Integer): 1
143ERROR
144end
145
146test "initializer keyword check" do
147 random = Random.new(1)
148
149 assert_equal random.begin, 1
150end
151
152test "default block" do
153 object = WithDefaultBlock.new
154 assert_equal object.block.call, "Hello"
155
156 object = WithDefaultBlock.new { "World" }
157 assert_equal object.block.call, "World"
158end
159
160test "default value (as a proc) executes in the context of the receiver" do
161 object = WithContextualDefault.new
162 assert_equal object.combined, "Hello World"
163end
164
165test "properties are enumerable" do
166 props = Person.literal_properties
167 assert_equal props.size, 2
168 assert_equal props.map(&:name), [:name, :age]
169
170 props = Empty.literal_properties
171 assert_equal props.size, 0
172end
173
174test "introspection" do
175 prop1, prop2 = *Person.literal_properties
176
177 assert_equal prop1.name, :name
178 assert_equal prop1.type, String
179
180 assert(prop1.positional?) { "Expected name to be kind :positional" }
181 refute(prop1.keyword?) { "Expected name to not be kind :keyword" }
182 refute(prop1.block?) { "Expected name to not be kind :&" }
183 refute(prop1.splat?) { "Expected name to not be bind :*" }
184 refute(prop1.double_splat?) { "Expected name to not be kind :**" }
185 assert(prop1.required?) { "Expected name to be required" }
186 refute(prop1.optional?) { "Expected name to not be optional" }
187
188 assert_equal prop2.name, :age
189 assert_equal prop2.type, Integer
190
191 assert(prop2.keyword?) { "Expected age to be kind :keyword" }
192 assert(prop2.required?) { "Expected age to be required" }
193
194 props = WithDefaultBlock.literal_properties
195 prop_block = props.first
196 assert(prop_block.block?) { "Expected block to be kind :&" }
197 assert(prop_block.optional?) { "Expected block to be optional" }
198
199 props = WithNilableType.literal_properties
200 prop_name = props.first
201 assert(prop_name.optional?) { "Expected name to be optional" }
202end
203
204test "after initialize callback" do
205 callback_called = false
206
207 public_callback = Class.new do
208 extend Literal::Properties
209
210 prop :name, String
211
212 define_method :after_initialize do
213 callback_called = true
214 end
215 end
216
217 public_callback.new(name: "John")
218
219 assert callback_called
220
221 callback_called = false
222
223 protected_callback = Class.new do
224 extend Literal::Properties
225
226 prop :name, String
227
228 define_method :after_initialize do
229 callback_called = true
230 end
231
232 protected :after_initialize
233 end
234
235 protected_callback.new(name: "John")
236
237 assert callback_called
238
239 callback_called = false
240
241 private_callback = Class.new do
242 extend Literal::Properties
243
244 prop :name, String
245
246 define_method :after_initialize do
247 callback_called = true
248 end
249
250 private :after_initialize
251 end
252
253 private_callback.new(name: "John")
254
255 assert callback_called
256
257 callback_called = false
258
259 empty = Class.new do
260 extend Literal::Properties
261
262 define_method :after_initialize do
263 callback_called = true
264 end
265 end
266
267 empty.new
268
269 assert callback_called
270end
271
272class Friend < Person
273 prop :age, Float, reader: :public
274end
275
276test "inheritance" do
277 friend = Friend.new("John", age: 30.5)
278
279 assert_equal friend.name, "John"
280 assert_equal friend.age, 30.5
281end
282
283class WithPredicate
284 extend Literal::Properties
285
286 prop :enabled, _Boolean, predicate: :public
287end
288
289test "predicates" do
290 enabled = WithPredicate.new(enabled: true)
291 disabled = WithPredicate.new(enabled: false)
292
293 assert_equal enabled.enabled?, true
294 assert_equal disabled.enabled?, false
295end
296
297class WithWriters < Example
298 extend Literal::Properties
299
300 prop :example, _Nilable(String), writer: :public
301 prop :a, _Nilable(_Array(String)), writer: :public
302end
303
304test "writer type error" do
305 instance = WithWriters.new
306
307 error = assert_raises(Literal::TypeError) do
308 instance.example = 0
309 end
310
311 assert_equal error.message, <<~ERROR
312 Type mismatch
313
314 #{WithWriters}#example=(value) (from #{error.backtrace[1]})
315 Expected: _Nilable(String)
316 Actual (Integer): 0
317ERROR
318
319 error = assert_raises(Literal::TypeError) do
320 instance.a = [1]
321 end
322
323 assert_equal error.message, <<~ERROR
324 Type mismatch
325
326 #{WithWriters}#a=(value) (from #{error.backtrace[1]})
327 [0]
328 Expected: String
329 Actual (Integer): 1
330ERROR
331end
332
333class Family
334 extend Literal::Properties
335
336 prop :members, _Array(_Map(person: Person, role: Symbol)), :positional, reader: :public
337 prop :last_reunion_year, _Nilable(Integer)
338end
339
340test "nested properties raise in initializer" do
341 error = assert_raises(Literal::TypeError) do
342 Family.new(
343 [
344 {
345 person: Person.new("Json", age: 1),
346 role: 1,
347 },
348 {
349 person: Person.new("John", age: 30),
350 role: "Father",
351 },
352 {
353 1 => 2,
354 },
355 ],
356 )
357 end
358
359 assert_equal error.message, <<~ERROR
360 Type mismatch
361
362 #{Family}#initialize (from #{error.backtrace[1]})
363 members
364 [0]
365 [:role]
366 Expected: Symbol
367 Actual (Integer): 1
368 [1]
369 [:role]
370 Expected: Symbol
371 Actual (String): "Father"
372 [2]
373 [:person]
374 Expected: #{Person.inspect}
375 Actual (NilClass): nil
376 [:role]
377 Expected: Symbol
378 Actual (NilClass): nil
379 ERROR
380
381 error = assert_raises(Literal::TypeError) { Family.new([1]) }
382
383 assert_equal error.message, <<~ERROR
384 Type mismatch
385
386 #{Family}#initialize (from #{error.backtrace[1]})
387 members
388 [0]
389 Expected: _Map(#{{ person: Person, role: Symbol }})
390 Actual (Integer): 1
391ERROR
392
393 error = assert_raises(Literal::TypeError) do
394 Family.new([], last_reunion_year: :two_thousand)
395 end
396
397 assert_equal error.message, <<~ERROR
398 Type mismatch
399
400 #{Family}#initialize (from #{error.backtrace[1]})
401 last_reunion_year:
402 Expected: _Nilable(Integer)
403 Actual (Symbol): :two_thousand
404 ERROR
405end
406
407test "nested properties succeed in initializer" do
408 refute_raises do
409 Family.new(
410 [
411 {
412 person: Person.new("Json", age: 1),
413 role: :son,
414 },
415 {
416 person: Person.new("John", age: 30),
417 role: :brother,
418 },
419 ],
420 )
421 end
422
423 refute_raises { Family.new([]) }
424 refute_raises { Family.new([], last_reunion_year: 0) }
425end
426
427test "#to_h" do
428 person = Person.new("John", age: 30)
429 assert_equal person.to_h, { name: "John", age: 30 }
430
431 empty = Empty.new
432 assert_equal empty.to_h, {}
433end