api_view 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.coco.yml +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +10 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +119 -0
- data/Rakefile +2 -0
- data/api_view.gemspec +36 -0
- data/benchmark_results.md +13 -0
- data/example/benchmark.rb +43 -0
- data/example/models/box_score.rb +7 -0
- data/example/models/event.rb +20 -0
- data/example/models/event_factory.rb +61 -0
- data/example/models/model.rb +11 -0
- data/example/models/play_by_play_record.rb +7 -0
- data/example/models/team.rb +7 -0
- data/example/require_models.rb +15 -0
- data/example/views/basketball/box_score.rb +11 -0
- data/example/views/basketball/event.rb +15 -0
- data/example/views/basketball/play_by_play_record.rb +4 -0
- data/example/views/basketball/team.rb +3 -0
- data/example/views/box_score.rb +5 -0
- data/example/views/event.rb +11 -0
- data/example/views/event_summary.rb +11 -0
- data/example/views/team.rb +4 -0
- data/lib/api_view.rb +5 -0
- data/lib/api_view/base.rb +88 -0
- data/lib/api_view/default.rb +20 -0
- data/lib/api_view/engine.rb +116 -0
- data/lib/api_view/registry.rb +20 -0
- data/lib/api_view/version.rb +3 -0
- data/sh/c +1 -0
- data/sh/env.rb +5 -0
- data/sh/test +19 -0
- data/sh/update_benchmark +33 -0
- data/test/base_test.rb +119 -0
- data/test/default_test.rb +30 -0
- data/test/engine_test.rb +42 -0
- data/test/registry_test.rb +24 -0
- data/test/test_helper.rb +32 -0
- metadata +285 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
class BasketballEventApiView < EventApiView
|
2
|
+
|
3
|
+
attributes :important, :location
|
4
|
+
main_object :event
|
5
|
+
|
6
|
+
def instance_convert
|
7
|
+
if event.ncaa? then
|
8
|
+
field :away_ranking, event.away_ranking
|
9
|
+
field :away_region, event.away_region
|
10
|
+
field :home_ranking, event.home_ranking
|
11
|
+
field :home_region, event.home_region
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class EventSummaryApiView < ::ApiView::Base
|
2
|
+
|
3
|
+
attributes :game_date, :game_type, :status
|
4
|
+
main_object :event
|
5
|
+
|
6
|
+
def instance_convert
|
7
|
+
field :away_team, event.away_team, via: BasketballTeamApiView
|
8
|
+
field :home_team, event.home_team, via: BasketballTeamApiView
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
data/lib/api_view.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
module ApiView
|
2
|
+
|
3
|
+
class Base < ::Hash
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def for_model(model)
|
8
|
+
ApiView::Registry.add_model(model, self)
|
9
|
+
end
|
10
|
+
|
11
|
+
def render(obj, scope={}, options={})
|
12
|
+
options[:use] = self
|
13
|
+
ApiView::Engine.render(obj, scope, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def parent_attributes
|
17
|
+
parent = self.superclass
|
18
|
+
return [] if parent.name == "ApiView::Base"
|
19
|
+
return parent.instance_variable_get(:@attributes)
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def main_object(main_object_name)
|
24
|
+
alias_method main_object_name, :object
|
25
|
+
end
|
26
|
+
|
27
|
+
# defines the basic (flat) fields that will be copied from the main object
|
28
|
+
def attributes(*attrs)
|
29
|
+
@attributes ||= []
|
30
|
+
@attributes = (@attributes + attrs).flatten
|
31
|
+
parent_attributes.reverse.each do |a|
|
32
|
+
@attributes.unshift(a) if not @attributes.include? a
|
33
|
+
end
|
34
|
+
|
35
|
+
# create a method which reads each attribute from the model object and
|
36
|
+
# copies it into the hash, then returns the hash itself
|
37
|
+
# e.g.,
|
38
|
+
# def collect_attributes
|
39
|
+
# self.store(:foo, @object.foo)
|
40
|
+
# ...
|
41
|
+
# self
|
42
|
+
# end
|
43
|
+
code = ["def collect_attributes()"]
|
44
|
+
@attributes.each do |a|
|
45
|
+
code << "self.store(:#{a}, @object.#{a})"
|
46
|
+
end
|
47
|
+
code << "end"
|
48
|
+
class_eval(code.join("\n"))
|
49
|
+
end
|
50
|
+
alias_method :attrs, :attributes
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
attr_reader :object
|
55
|
+
alias_method :obj, :object
|
56
|
+
|
57
|
+
def initialize(object)
|
58
|
+
super(nil)
|
59
|
+
@object = object
|
60
|
+
end
|
61
|
+
|
62
|
+
def collect_attributes
|
63
|
+
# no-op by default
|
64
|
+
end
|
65
|
+
|
66
|
+
# this is the method that is supposed to be overriden in the subclass
|
67
|
+
def instance_convert
|
68
|
+
# no-op by default, override in you subclass
|
69
|
+
end
|
70
|
+
|
71
|
+
def convert
|
72
|
+
collect_attributes()
|
73
|
+
instance_convert
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
# hides the details for serialization implementation
|
78
|
+
def field(fieldname, field_object, opts={})
|
79
|
+
serializer = opts[:via]
|
80
|
+
value = if serializer
|
81
|
+
serializer.new(field_object).convert
|
82
|
+
else
|
83
|
+
ApiView::Engine.convert(field_object)
|
84
|
+
end
|
85
|
+
store fieldname, value
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ApiView
|
2
|
+
class Default < Base
|
3
|
+
def self.convert(obj)
|
4
|
+
if obj.respond_to? :to_api then
|
5
|
+
obj.to_api
|
6
|
+
elsif obj.respond_to? :to_hash then
|
7
|
+
obj.to_hash
|
8
|
+
elsif obj.respond_to? :serializable_hash then
|
9
|
+
obj.serializable_hash
|
10
|
+
else
|
11
|
+
obj
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# delegate to class method
|
16
|
+
def convert
|
17
|
+
self.class.convert(object)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
|
2
|
+
module ApiView
|
3
|
+
class Engine
|
4
|
+
|
5
|
+
# Classes which require no further conversion
|
6
|
+
BASIC_TYPES = [
|
7
|
+
String, Integer, Fixnum, Bignum, Float,
|
8
|
+
TrueClass, FalseClass,
|
9
|
+
Time, Date, DateTime
|
10
|
+
]
|
11
|
+
BASIC_TYPES_LOOKUP = BASIC_TYPES.to_set
|
12
|
+
DEFAULT_FORMAT = 'json'.freeze
|
13
|
+
|
14
|
+
class << self
|
15
|
+
|
16
|
+
# Render the given object as JSON or XML
|
17
|
+
#
|
18
|
+
# @param [Object] obj
|
19
|
+
# @param [ActionDispatch::Request] scope
|
20
|
+
# @param [Hash] options
|
21
|
+
# @option options [String] :format Request a particular format ("json" or "xml")
|
22
|
+
#
|
23
|
+
# @return [String]
|
24
|
+
def render(obj, scope={}, options={})
|
25
|
+
ret = convert(obj, options)
|
26
|
+
# skip the serialization, useful for extra-speed in unit-tests
|
27
|
+
return ret if should_skip?(options)
|
28
|
+
|
29
|
+
# already converted (by default converter, for ex)
|
30
|
+
return ret if ret.kind_of? String
|
31
|
+
|
32
|
+
# TODO cache_results { self.send("to_" + format.to_s) }
|
33
|
+
format = options[:format] || self.request_format(scope)
|
34
|
+
self.send("to_" + format.to_s, ret)
|
35
|
+
end
|
36
|
+
|
37
|
+
def should_skip?(options)
|
38
|
+
options.fetch(:skip_serialization) { @skip_serialization }
|
39
|
+
end
|
40
|
+
|
41
|
+
def skip_serialization=(value)
|
42
|
+
@skip_serialization = value
|
43
|
+
end
|
44
|
+
|
45
|
+
# Convert the given object into a hash, array or other simple type
|
46
|
+
# (String, Fixnum, etc) that can be easily serialized into JSON or XML.
|
47
|
+
#
|
48
|
+
# @param [Object] obj
|
49
|
+
# @return [Object]
|
50
|
+
def convert(obj, options={})
|
51
|
+
return obj if is_basic_type?(obj)
|
52
|
+
return convert_hash(obj, options) if obj.kind_of?(Hash)
|
53
|
+
return convert_enumerable(obj, options) if obj.respond_to?(:map)
|
54
|
+
return convert_custom_type(obj, options)
|
55
|
+
end
|
56
|
+
|
57
|
+
def is_basic_type?(obj)
|
58
|
+
BASIC_TYPES_LOOKUP.include?(obj.class)
|
59
|
+
end
|
60
|
+
|
61
|
+
def convert_hash(obj, options)
|
62
|
+
ret = {}
|
63
|
+
obj.each{ |k,v| ret[k] = convert(v, options) }
|
64
|
+
ret
|
65
|
+
end
|
66
|
+
|
67
|
+
def convert_enumerable(obj, options)
|
68
|
+
if (options.count == 0) then
|
69
|
+
converter = converter_for(obj.first.class, options)
|
70
|
+
return obj.map { |o| converter.new(o).convert }
|
71
|
+
else
|
72
|
+
return obj.map { |o| convert(o, options) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def convert_custom_type(obj, options)
|
77
|
+
converter_for(obj.class, options).new(obj).convert
|
78
|
+
end
|
79
|
+
|
80
|
+
def converter_for(klazz, options)
|
81
|
+
ApiView::Registry.converter_for(klazz, options)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns a JSON representation of the data object
|
85
|
+
def to_json(obj)
|
86
|
+
MultiJson.dump(obj)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns an XML representation of the data object
|
90
|
+
def to_xml(obj)
|
91
|
+
obj.to_xml
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns a guess at the format in this scope
|
95
|
+
# request_format => "xml"
|
96
|
+
def request_format(scope)
|
97
|
+
format = format_from_params(scope)
|
98
|
+
format ||= format_from_request(scope)
|
99
|
+
return format if (format && self.respond_to?("to_#{format}"))
|
100
|
+
DEFAULT_FORMAT
|
101
|
+
end
|
102
|
+
|
103
|
+
def format_from_params(scope)
|
104
|
+
params = scope.respond_to?(:params) ? scope.params : {}
|
105
|
+
params[:format]
|
106
|
+
end
|
107
|
+
|
108
|
+
def format_from_request(scope)
|
109
|
+
request = scope.respond_to?(:request) && scope.request
|
110
|
+
return unless request
|
111
|
+
request.format.to_sym.to_s if request.respond_to?(:format)
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ApiView
|
2
|
+
class Registry
|
3
|
+
class << self
|
4
|
+
def models
|
5
|
+
@models ||= {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def add_model(model, converter)
|
9
|
+
models[model.to_s] = converter
|
10
|
+
end
|
11
|
+
|
12
|
+
def converter_for(clazz, options=nil)
|
13
|
+
if options && options[:use].kind_of?(Class) then
|
14
|
+
return options[:use]
|
15
|
+
end
|
16
|
+
models[clazz.to_s] || ApiView::Default
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/sh/env.rb
ADDED
data/sh/test
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rake/testtask'
|
3
|
+
|
4
|
+
pattern = "{test,lib/**/__tests__}/**/**_test.rb"
|
5
|
+
|
6
|
+
if ARGV[0]
|
7
|
+
regex = %r[#{ARGV[0]}]
|
8
|
+
all_tests = (Dir[pattern]).grep(regex)
|
9
|
+
end
|
10
|
+
Rake::TestTask.new("my_test") do |test|
|
11
|
+
if all_tests
|
12
|
+
test.test_files = all_tests
|
13
|
+
else
|
14
|
+
test.pattern = pattern
|
15
|
+
end
|
16
|
+
test.verbose = true
|
17
|
+
end
|
18
|
+
|
19
|
+
Rake::Task['my_test'].invoke
|
data/sh/update_benchmark
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
class Updater
|
4
|
+
def run
|
5
|
+
run_benchmark
|
6
|
+
fix_formatting
|
7
|
+
end
|
8
|
+
|
9
|
+
def run_benchmark
|
10
|
+
exec('ruby example/benchmark.rb > benchmark_results.md')
|
11
|
+
end
|
12
|
+
|
13
|
+
def fix_formatting
|
14
|
+
name = 'benchmark_results.md'
|
15
|
+
c = File.read(name)
|
16
|
+
new_c = c.split("\n").map{|l|
|
17
|
+
if l == ''
|
18
|
+
''
|
19
|
+
else
|
20
|
+
(" " * 4) + l
|
21
|
+
end
|
22
|
+
}.join("\n")
|
23
|
+
File.open(name, 'w') do |f|
|
24
|
+
f.puts new_c
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def exec(cmd)
|
29
|
+
`#{cmd}`
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Updater.new.run
|
data/test/base_test.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require './test/test_helper'
|
2
|
+
|
3
|
+
describe 'ApiView::Base' do
|
4
|
+
before do
|
5
|
+
ApiView::Engine.skip_serialization = true
|
6
|
+
end
|
7
|
+
after do
|
8
|
+
ApiView::Engine.skip_serialization = false
|
9
|
+
end
|
10
|
+
|
11
|
+
describe 'class methods' do
|
12
|
+
describe '#render' do
|
13
|
+
class RenderTestApiView < ::ApiView::Base
|
14
|
+
attributes :abbreviation, :full_name, :location
|
15
|
+
end
|
16
|
+
|
17
|
+
it "renders json by default" do
|
18
|
+
ApiView::Engine.skip_serialization = false
|
19
|
+
obj = OpenStruct.new(abbreviation: 'hey', full_name: 'full name', location: 'loc')
|
20
|
+
res = RenderTestApiView.render(obj)
|
21
|
+
expected = {"abbreviation"=>"hey", "full_name"=>"full name", "location"=>"loc"}
|
22
|
+
MultiJson.load(res).must_equal expected
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'serialization skipping' do
|
26
|
+
it "global skipping can be overriden for each render call" do
|
27
|
+
ApiView::Engine.skip_serialization = true
|
28
|
+
obj = OpenStruct.new(abbreviation: 'hey', full_name: 'full name', location: 'loc')
|
29
|
+
res = RenderTestApiView.render(obj, {}, {skip_serialization: false})
|
30
|
+
expected = "{\"abbreviation\":\"hey\",\"full_name\":\"full name\",\"location\":\"loc\"}"
|
31
|
+
res.must_equal expected
|
32
|
+
end
|
33
|
+
|
34
|
+
it "also returns a hash, if global serialization is skipped" do
|
35
|
+
ApiView::Engine.skip_serialization = true
|
36
|
+
obj = OpenStruct.new(abbreviation: 'hey', full_name: 'full name', location: 'loc')
|
37
|
+
res = RenderTestApiView.render(obj)
|
38
|
+
expected = {abbreviation: "hey", full_name: "full name", location: "loc"}
|
39
|
+
res.must_equal expected
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe '#main_object' do
|
45
|
+
class MainObjectApiView < ::ApiView::Base
|
46
|
+
main_object :main_obj
|
47
|
+
attributes :location
|
48
|
+
def instance_convert
|
49
|
+
field :hey, main_obj.no_hey
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
it "allows to call object by some custom name" do
|
54
|
+
obj = OpenStruct.new(location: 'some location', no_hey: 'some hey')
|
55
|
+
res = MainObjectApiView.render(obj)
|
56
|
+
res.must_equal({:location=>"some location", :hey=>"some hey"})
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'for_model' do
|
61
|
+
class ForModelModel
|
62
|
+
attr_accessor :a, :b
|
63
|
+
end
|
64
|
+
class ForModelApiView < ::ApiView::Base
|
65
|
+
main_object :main_obj
|
66
|
+
for_model ForModelModel
|
67
|
+
attributes :a, :b
|
68
|
+
end
|
69
|
+
|
70
|
+
it "registers this specific serializer with a specific model" do
|
71
|
+
ApiView::Registry.converter_for(ForModelModel).must_equal ForModelApiView
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
describe 'parent_attributes' do
|
77
|
+
parent = Class.new(ApiView::Base) do
|
78
|
+
attributes :a, :b
|
79
|
+
end
|
80
|
+
|
81
|
+
child = Class.new(parent) do
|
82
|
+
attributes :c
|
83
|
+
end
|
84
|
+
it "allows to use attributes from the parent class" do
|
85
|
+
obj = OpenStruct.new(a: 'a', b: 'b', c: 'c')
|
86
|
+
child.render(obj).must_equal({:a=>"a", :b=>"b", :c=>"c"})
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe 'instance methods' do
|
92
|
+
describe '#convert' do
|
93
|
+
class ConvertSimpleApiView < ::ApiView::Base
|
94
|
+
attributes :some_value
|
95
|
+
|
96
|
+
def instance_convert
|
97
|
+
field :simple, true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class ConvertTestApiView < ::ApiView::Base
|
102
|
+
attributes :abbreviation, :full_name, :location
|
103
|
+
|
104
|
+
def instance_convert
|
105
|
+
field :away_team, 'away_team'
|
106
|
+
field :simple_view, object.simple_view, via: ConvertSimpleApiView
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
it "works for nested serialization" do
|
111
|
+
simple_view = OpenStruct.new(some_value: 'very simple value')
|
112
|
+
obj = OpenStruct.new(abbreviation: 'hey', full_name: 'full name', location: 'loc', simple_view: simple_view)
|
113
|
+
res = ConvertTestApiView.render(obj)
|
114
|
+
expected = {:abbreviation=>"hey", :full_name=>"full name", :location=>"loc", :away_team=>"away_team", :simple_view=>{:some_value=>"very simple value", :simple=>true}}
|
115
|
+
res.must_equal expected
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|