Skip to content

Commit e5325d1

Browse files
danbernierchingor13
authored andcommitted
Dynamic attribute optimizations (#281)
* Add a unit test to benchmark dynamic attributes This obviously is the wrong place for this, but it lets me run things quickly. It'll come out before the end. * Check has_attribute? before regexes for setters Sample benchmark before: user system total real read: 1.070000 0.000000 1.070000 ( 1.077592) write: 1.130000 0.010000 1.140000 ( 1.142294) Sample benchmark after: user system total real read: 0.580000 0.010000 0.590000 ( 0.593090) write: 1.170000 0.000000 1.170000 ( 1.180000) * Use end_with? instead of regex engine Not sure this matters yet. * Use (& memoize) a DefaultKeyFormatter This avoids repeatedly checking whether the class responds to `#key_formatter`, calling `#key_formatter`, and checking whether its result is nil. Sample before benchmark: user system total real read: 0.580000 0.000000 0.580000 ( 0.576614) write: 1.130000 0.010000 1.140000 ( 1.141003) Sample after benchmark: user system total real read: 0.570000 0.000000 0.570000 ( 0.571892) write: 0.670000 0.010000 0.680000 ( 0.672004) * Auto-define basic attribute getters from method_missing Sample before benchmark: user system total real read: 0.570000 0.000000 0.570000 ( 0.576622) write: 0.670000 0.000000 0.670000 ( 0.672334) Sample after benchmark: user system total real read: 0.020000 0.000000 0.020000 ( 0.013004) write: 0.640000 0.000000 0.640000 ( 0.647726) Because this defines a method for an attribute when the attribute is accessed, I had to change the spec that checked for handling missing methods for attributes, because it was accessing the attribute, and therefore defining the method.
1 parent 5c16080 commit e5325d1

File tree

3 files changed

+72
-17
lines changed

3 files changed

+72
-17
lines changed

lib/json_api_client/helpers/dynamic_attributes.rb

+24-11
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def []=(key, value)
2424
end
2525

2626
def respond_to_missing?(method, include_private = false)
27-
if (method.to_s =~ /^(.*)=$/) || has_attribute?(method)
27+
if has_attribute?(method) || method.to_s.end_with?('=')
2828
true
2929
else
3030
super
@@ -38,16 +38,19 @@ def has_attribute?(attr_name)
3838
protected
3939

4040
def method_missing(method, *args, &block)
41-
normalized_method = if key_formatter
42-
key_formatter.unformat(method.to_s)
43-
else
44-
method.to_s
45-
end
46-
47-
if normalized_method =~ /^(.*)=$/
48-
set_attribute($1, args.first)
49-
elsif has_attribute?(method)
50-
attributes[method]
41+
if has_attribute?(method)
42+
self.class.class_eval do
43+
define_method(method) do
44+
attributes[method]
45+
end
46+
end
47+
return send(method)
48+
end
49+
50+
normalized_method = safe_key_formatter.unformat(method.to_s)
51+
52+
if normalized_method.end_with?('=')
53+
set_attribute(normalized_method[0..-2], args.first)
5154
else
5255
super
5356
end
@@ -61,10 +64,20 @@ def set_attribute(name, value)
6164
attributes[name] = value
6265
end
6366

67+
def safe_key_formatter
68+
@safe_key_formatter ||= (key_formatter || DefaultKeyFormatter.new)
69+
end
70+
6471
def key_formatter
6572
self.class.respond_to?(:key_formatter) && self.class.key_formatter
6673
end
6774

75+
class DefaultKeyFormatter
76+
def unformat(method)
77+
method.to_s
78+
end
79+
end
80+
6881
end
6982
end
7083
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
require 'test_helper'
2+
require 'benchmark'
3+
4+
class BenchmarkDynamicAttributesTest < MiniTest::Test
5+
def test_can_parse_global_meta_data
6+
stub_request(:get, "http://example.com/articles/1")
7+
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
8+
data: {
9+
type: "articles",
10+
id: "1",
11+
attributes: {
12+
title: "Rails is Omakase"
13+
}
14+
},
15+
meta: {
16+
copyright: "Copyright 2015 Example Corp.",
17+
authors: [
18+
"Yehuda Katz",
19+
"Steve Klabnik",
20+
"Dan Gebhardt"
21+
]
22+
},
23+
}.to_json)
24+
25+
article = Article.find(1).first
26+
27+
assert_equal "Rails is Omakase", article.title
28+
assert_equal "1", article.id
29+
30+
n = 10_000
31+
puts
32+
Benchmark.bm do |x|
33+
x.report('read: ') { n.times { article.title; article.id } }
34+
x.report('write:') do
35+
n.times do
36+
article.title = 'New title'
37+
article.better_title = 'Better title'
38+
end
39+
end
40+
end
41+
end
42+
end

test/unit/resource_test.rb

+6-6
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@ def test_formatted_key_accessors
7373
end
7474

7575
with_altered_config(Article, :json_key_format => :underscored_key) do
76-
article = Article.new("foo-bar" => "baz")
76+
article = Article.new("foo-bam" => "baz")
7777
# Does not recognize dasherized attributes, fall back to hash syntax
78-
refute article.respond_to? :foo_bar
79-
assert_equal("baz", article.send("foo-bar"))
80-
assert_equal("baz", article.send(:"foo-bar"))
81-
assert_equal("baz", article["foo-bar"])
82-
assert_equal("baz", article[:"foo-bar"])
78+
refute article.respond_to? :foo_bam
79+
assert_equal("baz", article.send("foo-bam"))
80+
assert_equal("baz", article.send(:"foo-bam"))
81+
assert_equal("baz", article["foo-bam"])
82+
assert_equal("baz", article[:"foo-bam"])
8383
end
8484
end
8585

0 commit comments

Comments
 (0)