Skip to content

Commit cb33fd1

Browse files
authored
Merge pull request #15 from fastruby/Issue-14-more-conventional-rails
Refactor model and controller to follow Rails conventions
2 parents 6c3a5ee + 0f52dec commit cb33fd1

File tree

7 files changed

+199
-125
lines changed

7 files changed

+199
-125
lines changed
Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,16 @@
11
class ReportsController < ApplicationController
2+
before_action :fix_missing_json_content_type
3+
4+
wrap_parameters false
5+
26
include ReportsHelper
37

48
protect_from_forgery :except => [:create]
59

6-
DATA_KEY = %W!central_tendency error name ips stddev microseconds iterations cycles!
7-
810
def create
9-
data = request.body.read
10-
11-
begin
12-
input = JSON.parse data
13-
rescue Exception => err
14-
logger.fatal("Error: #{err.message} || #{data}")
15-
head 400
16-
return
17-
end
18-
19-
ary = input["entries"]
20-
21-
unless ary.kind_of? Array
22-
head 400
23-
return
24-
end
25-
26-
ary.each do |j|
27-
needed = DATA_KEY.dup
28-
29-
j.keys.each do |k|
30-
if DATA_KEY.include? k
31-
needed.delete k
32-
else
33-
head 400
34-
return
35-
end
36-
end
11+
rep = Report.create! report_params
3712

38-
unless needed.empty?
39-
head 400
40-
return
41-
end
42-
end
43-
44-
rep = Report.create report: JSON.generate(ary),
45-
ruby: input["ruby"],
46-
os: input["os"],
47-
arch: input["arch"]
48-
49-
options = input["options"] || {}
13+
options = params["options"] || {}
5014

5115
if options["compare"]
5216
rep.compare = true
@@ -55,6 +19,8 @@ def create
5519
rep.save
5620

5721
render json: { id: rep.short_id }
22+
rescue ActionController::ParameterMissing, ActiveRecord::RecordInvalid
23+
head 400
5824
end
5925

6026
def show
@@ -64,7 +30,7 @@ def show
6430
fastest_val = nil
6531
note_high_stddev = false
6632

67-
@report.data.each do |part|
33+
@report.entries.each do |part|
6834
if !fastest_val || part["ips"] > fastest_val
6935
fastest = part
7036
fastest_val = part["ips"]
@@ -78,4 +44,31 @@ def show
7844
@note_high_stddev = note_high_stddev
7945
@fastest = fastest
8046
end
47+
48+
private
49+
def report_params
50+
entries_params = params.require(:entries).map do |entry|
51+
entry.permit(:name, :ips, :central_tendency, :error, :stddev, :microseconds, :iterations, :cycles)
52+
end
53+
params.permit(:ruby, :os, :arch).merge(report: entries_params)
54+
end
55+
56+
# benchmark-ips sends the JSON string in the request body but it does not specify a content type
57+
# when that happens, Rails parses it incorrectly and produces a params key `"{\"entries\":"`
58+
#
59+
# we can detect this and fix the params object to properly parse the request body as JSON and update
60+
# the `params` object
61+
def fix_missing_json_content_type
62+
if params.keys.include?("{\"entries\":") && request.body.present?
63+
begin
64+
json = JSON.parse(request.body.read)
65+
params.delete("{\"entries\":")
66+
params.merge!(json)
67+
rescue JSON::ParserError
68+
# Body was not valid JSON, do nothing
69+
ensure
70+
request.body.rewind
71+
end
72+
end
73+
end
8174
end

app/models/report.rb

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
class Report < ActiveRecord::Base
22
ALPHABET = "123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ"
33
BASE = ALPHABET.length
4+
REQUIRED_ENTRY_ATTRIBUTES = %W(name ips stddev microseconds iterations cycles)
45

5-
def data
6-
JSON.parse report
7-
end
6+
serialize :report, coder: JSON
7+
alias_attribute :entries, :report
8+
validates :entries, presence: true
9+
validate :validate_entries_attributes
810

911
def short_id
1012
int_val = self.id
@@ -28,4 +30,27 @@ def self.find_from_short_id(base58_val)
2830

2931
Report.find int_val
3032
end
33+
34+
private
35+
def validate_entries_attributes
36+
return if entries.blank?
37+
38+
invalid_entries = entries.reject { |entry| valid_entry?(entry) }
39+
40+
if invalid_entries.any?
41+
errors.add(:entries, "missing attributes: #{ invalid_entries.map { |entry| error_message(entry) }.join(", ")}")
42+
end
43+
end
44+
45+
def valid_entry?(entry)
46+
REQUIRED_ENTRY_ATTRIBUTES.all? do |attr|
47+
entry[attr].present?
48+
end
49+
end
50+
51+
def error_message(entry)
52+
missing_attributes = REQUIRED_ENTRY_ATTRIBUTES - entry.keys
53+
54+
"#{entry} (#{missing_attributes.join(", ")})"
55+
end
3156
end

app/views/reports/show.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<% end %>
88
<th>iterations/second</th>
99
</tr>
10-
<% @report.data.each do |part| %>
10+
<% @report.entries.each do |part| %>
1111
<tr>
1212
<td>
1313
<% if part["name"] == @fastest["name"] %>

test/controllers/reports_controller_test.rb

Lines changed: 59 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
require 'test_helper'
1+
require "test_helper"
22

33
class ReportsControllerTest < ActionController::TestCase
4-
test "validates the data json" do
4+
test "errors if body is not json" do
55
data = "nope"
66

77
post :create, body: data
@@ -10,101 +10,92 @@ class ReportsControllerTest < ActionController::TestCase
1010
end
1111

1212
test "validates the data json as valid benchmark data" do
13-
data = <<-DATA
14-
{
15-
"entries":
16-
[{
17-
"name": "test",
18-
"ips": 10.1,
19-
"central_tendency": 10.1,
20-
"error": 23666,
21-
"stddev": 0.3,
22-
"microseconds": 3322,
23-
"iterations": 221,
24-
"cycles": 16
25-
}]
26-
}
27-
DATA
13+
data = {
14+
entries: [{
15+
name: "test",
16+
ips: 10.1,
17+
central_tendency: 10.1,
18+
error: 23666,
19+
stddev: 0.3,
20+
microseconds: 3322,
21+
iterations: 221,
22+
cycles: 16
23+
}]
24+
}
2825

29-
post :create, body: data
26+
post :create, params: data, as: :json
3027

3128
assert_equal "200", @response.code
3229

3330
rep = JSON.parse @response.body
3431

3532
report = Report.find_from_short_id rep["id"]
3633

37-
raw = <<-DATA
38-
[{
39-
"name": "test",
40-
"ips": 10.1,
41-
"central_tendency": 10.1,
42-
"error": 23666,
43-
"stddev": 0.3,
44-
"microseconds": 3322,
45-
"iterations": 221,
46-
"cycles": 16
47-
}]
48-
DATA
49-
50-
assert_equal JSON.parse(raw), report.data
34+
raw = <<~DATA
35+
[{
36+
"name": "test",
37+
"ips": 10.1,
38+
"central_tendency": 10.1,
39+
"error": 23666,
40+
"stddev": 0.3,
41+
"microseconds": 3322,
42+
"iterations": 221,
43+
"cycles": 16
44+
}]
45+
DATA
46+
47+
assert_equal JSON.parse(raw), report.entries
5148
end
5249

5350
test "errors on unknown data keys" do
54-
data = <<-DATA
55-
{
56-
"entries":
57-
[{
58-
"name": "test",
59-
"ipx": 10.1,
60-
"stddev": 0.3,
61-
"microseconds": 3322,
62-
"iterations": 221,
63-
"cycles": 16
64-
}]
65-
}
66-
DATA
51+
data = {
52+
entries: [{
53+
name: "test",
54+
ipx: 10.1,
55+
stddev: 0.3,
56+
microseconds: 3322,
57+
iterations: 221,
58+
cycles: 16
59+
}]
60+
}
6761

68-
post :create, body: data
62+
post :create, params: data, as: :json
6963

7064
assert_equal "400", @response.code
7165
end
7266

7367
test "errors out if there are keys missing" do
74-
data = <<-DATA
75-
{
76-
"entries":
77-
[{
78-
"name": "test"
79-
}]
80-
}
81-
DATA
68+
data = {
69+
entries: [{
70+
name: "test"
71+
}]
72+
}
8273

83-
post :create, body: data
74+
post :create, params: data, as: :json
8475

8576
assert_equal "400", @response.code
8677
end
8778

8879
test "environment variables are sent, processed, and saved" do
8980
data = {
90-
"ruby" => "3.2.1",
91-
"os" => "Linux",
92-
"arch" => "x86_64",
93-
"entries" => [
81+
ruby: "3.2.1",
82+
os: "Linux",
83+
arch: "x86_64",
84+
entries: [
9485
{
95-
"name" => "test",
96-
"ips" => 10.1,
97-
"central_tendency" => 10.1,
98-
"error" => 23666,
99-
"stddev" => 0.3,
100-
"microseconds" => 3322,
101-
"iterations" => 221,
102-
"cycles" => 16
86+
name: "test",
87+
ips: 10.1,
88+
central_tendency: 10.1,
89+
error: 23666,
90+
stddev: 0.3,
91+
microseconds: 3322,
92+
iterations: 221,
93+
cycles: 16
10394
}
10495
]
10596
}
10697

107-
post :create, body: data.to_json
98+
post :create, params: data, as: :json
10899

109100
rep = JSON.parse @response.body
110101

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
require 'test_helper'
2+
3+
class CreateReportTest < ActionDispatch::IntegrationTest
4+
test "process json body with missing content_type" do
5+
raw_json = "{\"entries\":[{\"name\":\"addition\",\"central_tendency\":27474451.540838767,\"ips\":27474451.540838767,\"error\":122038,\"stddev\":122038,\"microseconds\":1087174.973022461,\"iterations\":29868949,\"cycles\":2715359}],\"options\":{\"compare\":true}}"
6+
7+
# at the point of writing this, benchmark-ips makes this request with a raw string and no content type
8+
# this mimics that scenario
9+
post "/reports", params: raw_json, headers: { "CONTENT_LENGTH" => raw_json.length }
10+
11+
assert_equal 200, status
12+
end
13+
end

test/integration/routing_test.rb

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,14 @@
22

33
class RoutingTest < ActionDispatch::IntegrationTest
44
def test_short_id
5-
data = <<-DATA
6-
[{
7-
"name": "test",
8-
"ips": 10.1,
9-
"stddev": 0.3,
10-
"microseconds": 3322,
11-
"iterations": 221,
12-
"cycles": 16
13-
}]
14-
DATA
5+
data = [{
6+
name: "test",
7+
ips: 10.1,
8+
stddev: 0.3,
9+
microseconds: 3322,
10+
iterations: 221,
11+
cycles: 16
12+
}]
1513

1614
report = Report.create! report: data
1715

0 commit comments

Comments
 (0)