Skip to content

Commit 78d113b

Browse files
committed
perf: optimize type coercion and object conversion
Use set for boolean checks, cache lookups, extract constants.
1 parent fd3ccda commit 78d113b

5 files changed

Lines changed: 47 additions & 8 deletions

File tree

benchmarks/perf_comparison.rb

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,23 @@
2929
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
3030
require "structure"
3131

32-
# Test data with coercion
32+
# Test data with more coercions and nested structures
3333
TEST_DATA = {
3434
"attr1" => "foo",
35-
"attr2" => "123",
35+
"attr2" => "123", # Integer coercion
3636
"attr3" => "bar",
37-
"attr4" => "true",
37+
"attr4" => "true", # Boolean coercion
3838
"attr5" => ["a", "b", "c"],
3939
"attr6" => { "x" => "y" },
4040
"attr8" => "optional",
41+
"attr9" => "3.14", # Float coercion
42+
"attr10" => "42", # Integer coercion
43+
"attr11" => "1", # Boolean coercion
44+
"items" => [ # Array of nested structures
45+
{ "name" => "Item 1", "price" => "10.99" },
46+
{ "name" => "Item 2", "price" => "20.50" },
47+
{ "name" => "Item 3", "price" => "15.25" },
48+
],
4149
}.freeze
4250

4351
# Define dry-struct models
@@ -48,6 +56,13 @@ module Types
4856
include Dry.Types()
4957
end
5058

59+
class Item < Dry::Struct
60+
transform_keys(&:to_sym)
61+
62+
attribute :name, Types::String
63+
attribute :price, Types::Coercible::Float
64+
end
65+
5166
class Model < Dry::Struct
5267
transform_keys(&:to_sym)
5368

@@ -59,11 +74,20 @@ class Model < Dry::Struct
5974
attribute :attr6, Types::Hash
6075
attribute :attr7, Types::String.default("default")
6176
attribute? :attr8, Types::String
77+
attribute :attr9, Types::Coercible::Float
78+
attribute :attr10, Types::Coercible::Integer
79+
attribute :attr11, Types::Params::Bool
80+
attribute :items, Types::Array.of(Item)
6281
end
6382
end
6483

6584
# Define Structure models
6685
module StructureModels
86+
Item = Structure.new do
87+
attribute(:name, String)
88+
attribute(:price, Float)
89+
end
90+
6791
Model = Structure.new do
6892
attribute(:attr1, String)
6993
attribute(:attr2, Integer)
@@ -73,6 +97,10 @@ module StructureModels
7397
attribute(:attr6)
7498
attribute(:attr7, String, default: "default")
7599
attribute?(:attr8, String)
100+
attribute(:attr9, Float)
101+
attribute(:attr10, Integer)
102+
attribute(:attr11, :boolean)
103+
attribute(:items, ["Item"])
76104
end
77105
end
78106

lib/structure.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
# A library for parsing data into immutable Ruby Data objects with type coercion
66
module Structure
7+
TO_H_CHECKER = ->(x) { x.respond_to?(:to_h) && x }
8+
private_constant :TO_H_CHECKER
9+
710
class << self
811
# Creates a new Data class with attribute definitions and type coercion
912
#
@@ -79,7 +82,7 @@ def new(&block)
7982
v = public_send(m)
8083
value = case v
8184
when Array then v.map { |x| x.respond_to?(:to_h) && x ? x.to_h : x }
82-
when ->(x) { x.respond_to?(:to_h) && x } then v.to_h
85+
when TO_H_CHECKER then v.to_h
8386
else v
8487
end
8588
[m, value]
@@ -103,6 +106,7 @@ def new(&block)
103106
final = {}
104107
mappings = __structure_meta__[:mappings]
105108
defaults = __structure_meta__[:defaults]
109+
coercions = __structure_meta__[:coercions]
106110
after_parse = __structure_meta__[:after_parse]
107111
required = __structure_meta__[:required]
108112

@@ -122,7 +126,7 @@ def new(&block)
122126
end
123127

124128
if value
125-
coercion = __structure_meta__[:coercions][attr]
129+
coercion = coercions[attr]
126130
value = coercion.call(value) if coercion
127131
end
128132

lib/structure/types.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# frozen_string_literal: true
22

3+
require "set"
4+
35
module Structure
46
# Type coercion methods for converting values to specific types
57
module Types
68
class << self
79
# Rails-style boolean truthy values
810
# Reference: https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
9-
BOOLEAN_TRUTHY = [true, 1, "1", "t", "T", "true", "TRUE", "on", "ON"].freeze
11+
BOOLEAN_TRUTHY = Set.new([true, 1, "1", "t", "T", "true", "TRUE", "on", "ON"]).freeze
1012
private_constant :BOOLEAN_TRUTHY
1113

1214
# Main factory method for creating type coercers
@@ -93,12 +95,15 @@ def array(element_type, context = nil)
9395
value.map { |element| context.parse(element) }
9496
end
9597
when String
98+
resolved_class = nil
99+
96100
lambda do |value|
97101
unless value.respond_to?(:map)
98102
raise TypeError, "can't convert #{value.class} into Array"
99103
end
100104

101-
resolved_class = resolve_class(element_type, context)
105+
resolved_class ||= resolve_class(element_type, context)
106+
# @type var resolved_class: untyped
102107
value.map { |element| resolved_class.parse(element) }
103108
end
104109
else

sig/structure.rbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
module Structure
2+
TO_H_CHECKER: Proc
3+
24
interface _StructuredDataClass
35
def __structure_meta__: () -> {
46
types: Hash[Symbol, untyped],

sig/structure/types.rbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module Structure
44
def parse: (untyped) -> untyped
55
end
66

7-
BOOLEAN_TRUTHY: Array[untyped]
7+
BOOLEAN_TRUTHY: Set[untyped]
88

99
self.@boolean: Proc
1010
self.@kernel_cache: Hash[untyped, Proc]

0 commit comments

Comments
 (0)