Skip to content
This repository has been archived by the owner on Jun 10, 2022. It is now read-only.

Commit

Permalink
Dynamic attribute optimizations (JsonApiClient#281)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
danbernier authored and chingor13 committed Apr 13, 2018
1 parent 5c16080 commit e5325d1
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 17 deletions.
35 changes: 24 additions & 11 deletions lib/json_api_client/helpers/dynamic_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def []=(key, value)
end

def respond_to_missing?(method, include_private = false)
if (method.to_s =~ /^(.*)=$/) || has_attribute?(method)
if has_attribute?(method) || method.to_s.end_with?('=')
true
else
super
Expand All @@ -38,16 +38,19 @@ def has_attribute?(attr_name)
protected

def method_missing(method, *args, &block)
normalized_method = if key_formatter
key_formatter.unformat(method.to_s)
else
method.to_s
end

if normalized_method =~ /^(.*)=$/
set_attribute($1, args.first)
elsif has_attribute?(method)
attributes[method]
if has_attribute?(method)
self.class.class_eval do
define_method(method) do
attributes[method]
end
end
return send(method)
end

normalized_method = safe_key_formatter.unformat(method.to_s)

if normalized_method.end_with?('=')
set_attribute(normalized_method[0..-2], args.first)
else
super
end
Expand All @@ -61,10 +64,20 @@ def set_attribute(name, value)
attributes[name] = value
end

def safe_key_formatter
@safe_key_formatter ||= (key_formatter || DefaultKeyFormatter.new)
end

def key_formatter
self.class.respond_to?(:key_formatter) && self.class.key_formatter
end

class DefaultKeyFormatter
def unformat(method)
method.to_s
end
end

end
end
end
42 changes: 42 additions & 0 deletions test/unit/benchmark_dynamic_attributes_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
require 'test_helper'
require 'benchmark'

class BenchmarkDynamicAttributesTest < MiniTest::Test
def test_can_parse_global_meta_data
stub_request(:get, "http://example.com/articles/1")
.to_return(headers: {content_type: "application/vnd.api+json"}, body: {
data: {
type: "articles",
id: "1",
attributes: {
title: "Rails is Omakase"
}
},
meta: {
copyright: "Copyright 2015 Example Corp.",
authors: [
"Yehuda Katz",
"Steve Klabnik",
"Dan Gebhardt"
]
},
}.to_json)

article = Article.find(1).first

assert_equal "Rails is Omakase", article.title
assert_equal "1", article.id

n = 10_000
puts
Benchmark.bm do |x|
x.report('read: ') { n.times { article.title; article.id } }
x.report('write:') do
n.times do
article.title = 'New title'
article.better_title = 'Better title'
end
end
end
end
end
12 changes: 6 additions & 6 deletions test/unit/resource_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ def test_formatted_key_accessors
end

with_altered_config(Article, :json_key_format => :underscored_key) do
article = Article.new("foo-bar" => "baz")
article = Article.new("foo-bam" => "baz")
# Does not recognize dasherized attributes, fall back to hash syntax
refute article.respond_to? :foo_bar
assert_equal("baz", article.send("foo-bar"))
assert_equal("baz", article.send(:"foo-bar"))
assert_equal("baz", article["foo-bar"])
assert_equal("baz", article[:"foo-bar"])
refute article.respond_to? :foo_bam
assert_equal("baz", article.send("foo-bam"))
assert_equal("baz", article.send(:"foo-bam"))
assert_equal("baz", article["foo-bam"])
assert_equal("baz", article[:"foo-bam"])
end
end

Expand Down

0 comments on commit e5325d1

Please sign in to comment.