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.
@@ -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,4 @@
1
+ class BasketballPlayByPlayRecordApiView < ::ApiView::Base
2
+ for_model ::PlayByPlayRecord
3
+ attributes :points_type, :player_fouls, :player_score, :record_type, :seconds
4
+ end
@@ -0,0 +1,3 @@
1
+ class BasketballTeamApiView < TeamApiView
2
+ attributes :medium_name, :short_name
3
+ end
@@ -0,0 +1,5 @@
1
+
2
+ class BoxScoreApiView < ::ApiView::Base
3
+ for_model ::BoxScore
4
+ attributes :has_statistics, :progress
5
+ end
@@ -0,0 +1,11 @@
1
+ class EventApiView < EventSummaryApiView
2
+
3
+ for_model ::Event
4
+ attributes :share_url, :sport_name
5
+ main_object :event
6
+
7
+ def instance_convert
8
+ field :box_score, event.box_score, via: BasketballBoxScoreApiView
9
+ end
10
+
11
+ 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
@@ -0,0 +1,4 @@
1
+ class TeamApiView < ::ApiView::Base
2
+ for_model ::Team
3
+ attributes :abbreviation, :full_name, :location
4
+ end
@@ -0,0 +1,5 @@
1
+ require "api_view/version"
2
+ require 'api_view/engine'
3
+ require 'api_view/base'
4
+ require 'api_view/default'
5
+ require 'api_view/registry'
@@ -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
@@ -0,0 +1,3 @@
1
+ module ApiView
2
+ VERSION = "0.5.0"
3
+ end
data/sh/c ADDED
@@ -0,0 +1 @@
1
+ irb -r ./sh/env.rb
@@ -0,0 +1,5 @@
1
+ require 'bundler'
2
+ Bundler.setup
3
+ require 'api_view'
4
+ require './example/require_models'
5
+ puts "API VIEW LOADED"
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
@@ -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
@@ -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