benjaminkrause-restful 0.2.8
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.
- data/CHANGES.markdown +8 -0
- data/LICENSE.markdown +22 -0
- data/README.markdown +123 -0
- data/Rakefile +22 -0
- data/TODO.markdown +9 -0
- data/init.rb +1 -0
- data/lib/restful.rb +65 -0
- data/lib/restful/apimodel/attribute.rb +17 -0
- data/lib/restful/apimodel/collection.rb +22 -0
- data/lib/restful/apimodel/link.rb +21 -0
- data/lib/restful/apimodel/map.rb +41 -0
- data/lib/restful/apimodel/resource.rb +23 -0
- data/lib/restful/converters/active_record.rb +131 -0
- data/lib/restful/rails.rb +22 -0
- data/lib/restful/rails/action_controller.rb +14 -0
- data/lib/restful/rails/active_record/configuration.rb +167 -0
- data/lib/restful/rails/active_record/metadata_tools.rb +106 -0
- data/lib/restful/serializers/atom_like_serializer.rb +51 -0
- data/lib/restful/serializers/base.rb +57 -0
- data/lib/restful/serializers/hash_serializer.rb +59 -0
- data/lib/restful/serializers/json_serializer.rb +20 -0
- data/lib/restful/serializers/params_serializer.rb +46 -0
- data/lib/restful/serializers/xml_serializer.rb +161 -0
- data/rails/init.rb +1 -0
- data/restful.gemspec +17 -0
- data/test/converters/active_record_converter_test.rb +122 -0
- data/test/converters/basic_types_converter_test.rb +48 -0
- data/test/fixtures/models/paginated_collection.rb +4 -0
- data/test/fixtures/models/person.rb +29 -0
- data/test/fixtures/models/pet.rb +5 -0
- data/test/fixtures/models/wallet.rb +5 -0
- data/test/fixtures/people.json.yaml +94 -0
- data/test/fixtures/people.xml.yaml +123 -0
- data/test/fixtures/pets.json.yaml +20 -0
- data/test/fixtures/pets.xml.yaml +31 -0
- data/test/rails/active_record_metadata_test.rb +23 -0
- data/test/rails/configuration_test.rb +40 -0
- data/test/rails/restful_publish_test.rb +52 -0
- data/test/serializers/atom_serializer_test.rb +33 -0
- data/test/serializers/json_serializer_test.rb +82 -0
- data/test/serializers/params_serializer_test.rb +76 -0
- data/test/serializers/xml_serializer_test.rb +51 -0
- data/test/test_helper.rb +147 -0
- metadata +98 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'restful/serializers/base'
|
2
|
+
require 'yajl'
|
3
|
+
|
4
|
+
#
|
5
|
+
# AR params hash.
|
6
|
+
#
|
7
|
+
module Restful
|
8
|
+
module Serializers
|
9
|
+
class JsonSerializer < Base
|
10
|
+
|
11
|
+
serializer_name :json
|
12
|
+
|
13
|
+
def serialize(resource, options = {})
|
14
|
+
hasher = Restful::Serializers::HashSerializer.new
|
15
|
+
hash = hasher.serialize(resource, options)
|
16
|
+
Yajl::Encoder.encode(hash)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'restful/serializers/base'
|
2
|
+
require 'builder'
|
3
|
+
|
4
|
+
#
|
5
|
+
# AR params hash.
|
6
|
+
#
|
7
|
+
module Restful
|
8
|
+
module Serializers
|
9
|
+
class ParamsSerializer < Base
|
10
|
+
|
11
|
+
serializer_name :params
|
12
|
+
|
13
|
+
def serialize(resource, options = {})
|
14
|
+
params = {}
|
15
|
+
resource.values.each do |value|
|
16
|
+
if value.type == :collection # serialize the stuffs
|
17
|
+
resources = value.value
|
18
|
+
next if resources.empty?
|
19
|
+
name = resources.first.name.pluralize
|
20
|
+
|
21
|
+
array = []
|
22
|
+
resources.each do |resource|
|
23
|
+
array << serialize(resource)
|
24
|
+
end
|
25
|
+
|
26
|
+
params["#{paramify_keys(value.name)}_attributes".to_sym] = array
|
27
|
+
elsif value.type == :link
|
28
|
+
params[paramify_keys(value.name).to_sym] = Restful::Rails.tools.dereference(value.value)
|
29
|
+
elsif value.type == :resource
|
30
|
+
params["#{paramify_keys(value.name)}_attributes".to_sym] = serialize(value)
|
31
|
+
else # plain ole
|
32
|
+
params[paramify_keys(value.name).to_sym] = value.value # no need to format dates etc - just pass objects through.
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
params
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def paramify_keys(original_key)
|
42
|
+
original_key.to_s.tr("-", "_")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'restful/serializers/base'
|
2
|
+
require "rexml/document"
|
3
|
+
require 'builder'
|
4
|
+
require 'ruby-debug'
|
5
|
+
|
6
|
+
#
|
7
|
+
# Converts an APIModel to and from XML.
|
8
|
+
#
|
9
|
+
module Restful
|
10
|
+
module Serializers
|
11
|
+
class XMLSerializer < Base
|
12
|
+
|
13
|
+
serializer_name :xml
|
14
|
+
|
15
|
+
def serialize(resource, options = {})
|
16
|
+
|
17
|
+
xml = options[:builder] || Builder::XmlMarkup.new(:indent => 2)
|
18
|
+
xml.instruct! unless options[:instruct].is_a?(FalseClass)
|
19
|
+
|
20
|
+
raise NotImplementedError.new("xml serialization of maps has not been implemented. ") if resource.class == Restful::ApiModel::Map
|
21
|
+
|
22
|
+
if resource.is_a?(Restful::ApiModel::Collection)
|
23
|
+
add_collection(resource, xml, show_as_array = false)
|
24
|
+
else
|
25
|
+
xml.tag!(*root_element(resource)) do
|
26
|
+
add_link_to(resource, xml, :self => true)
|
27
|
+
|
28
|
+
resource.values.each do |value|
|
29
|
+
if value.type == :collection # serialize the stuffs
|
30
|
+
add_collection(value, xml)
|
31
|
+
elsif value.type == :link
|
32
|
+
add_link_to(value, xml)
|
33
|
+
elsif value.type == :resource
|
34
|
+
serialize(value, {:instruct => false, :builder => xml})
|
35
|
+
else # plain ole
|
36
|
+
add_tag(xml, value)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# returns a resource, or collection of resources.
|
44
|
+
def deserialize(xml, options = {})
|
45
|
+
build_resource(REXML::Document.new(xml).root)
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
def add_collection(value, xml, show_as_array = true)
|
51
|
+
resources = value.value
|
52
|
+
if first_resource = resources.first
|
53
|
+
xml.tag!(first_resource.name.pluralize, (show_as_array ? collections_decorations : {})) do
|
54
|
+
resources.each do |resource|
|
55
|
+
serialize(resource, { :instruct => false, :builder => xml })
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def add_link_to(resource, builder, options = {})
|
62
|
+
is_self = !!options[:self]
|
63
|
+
|
64
|
+
attributes = {:type => "link"}
|
65
|
+
attributes.merge!(:nil => "true") if resource.full_url.blank?
|
66
|
+
builder.tag!((is_self ? "restful-url" : transform_link_name(resource.name)), resource.full_url, attributes)
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_tag(builder, value)
|
70
|
+
|
71
|
+
if value.extended_type == :hash
|
72
|
+
build_hash(builder, value)
|
73
|
+
else
|
74
|
+
builder.tag!(value.name.to_s.dasherize, formatted_value(value), decorations(value))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def build_hash(builder, value)
|
79
|
+
builder.tag!(value.name.to_s.dasherize) do
|
80
|
+
value.value.each do |k, v|
|
81
|
+
builder.tag! k.to_s.dasherize, v
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def decorations(value)
|
87
|
+
decorations = {}
|
88
|
+
|
89
|
+
if value.extended_type == :binary
|
90
|
+
decorations[:encoding] = 'base64'
|
91
|
+
end
|
92
|
+
|
93
|
+
if value.extended_type != :string and value.extended_type != :notype
|
94
|
+
decorations[:type] = value.extended_type
|
95
|
+
end
|
96
|
+
|
97
|
+
if value.extended_type == :datetime
|
98
|
+
decorations[:type] = :datetime
|
99
|
+
end
|
100
|
+
|
101
|
+
if value.value.nil?
|
102
|
+
decorations[:nil] = true
|
103
|
+
end
|
104
|
+
|
105
|
+
if value.value.is_a?(FalseClass) || value.value.is_a?(TrueClass)
|
106
|
+
decorations[:type] = :boolean
|
107
|
+
end
|
108
|
+
|
109
|
+
decorations
|
110
|
+
end
|
111
|
+
|
112
|
+
def collections_decorations
|
113
|
+
{ :type => "array" }
|
114
|
+
end
|
115
|
+
|
116
|
+
def root_element(resource, options = {})
|
117
|
+
[resource.name]
|
118
|
+
end
|
119
|
+
|
120
|
+
# turns a rexml node into a Resource
|
121
|
+
def build_resource(node)
|
122
|
+
resource = root_resource(node)
|
123
|
+
|
124
|
+
node.elements.each do |el|
|
125
|
+
type = calculate_node_type(el)
|
126
|
+
resource.values << case type
|
127
|
+
|
128
|
+
when :link : build_link(el, type)
|
129
|
+
when :datetime
|
130
|
+
Restful.attr(el.name, DateTime.parse(el.text), type)
|
131
|
+
when :resource
|
132
|
+
build_resource(el)
|
133
|
+
when :array
|
134
|
+
Restful.collection(el.name, el.elements.map { |child| build_resource(child) }, type)
|
135
|
+
else
|
136
|
+
Restful.attr(el.name, el.text, type)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
resource
|
141
|
+
end
|
142
|
+
|
143
|
+
def calculate_node_type(el)
|
144
|
+
if el.children.size > 1 && el.attributes["type"].blank?
|
145
|
+
return :resource
|
146
|
+
else
|
147
|
+
(el.attributes["type"] || "string").to_sym
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def build_link(el, type)
|
152
|
+
Restful.link(revert_link_name(el.name), nil, el.text, type)
|
153
|
+
end
|
154
|
+
|
155
|
+
def root_resource(node)
|
156
|
+
url = node.delete_element("restful-url").try(:text)
|
157
|
+
Restful.resource(node.name, :url => url)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'restful'
|
data/restful.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "restful"
|
3
|
+
s.version = "0.2.8"
|
4
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
5
|
+
s.authors = ["Daniel Bornkessel", "Rany Keddo"]
|
6
|
+
s.date = "2009-08-11"
|
7
|
+
s.email = "M4SSIVE@m4ssive.com"
|
8
|
+
s.extra_rdoc_files = %w{ README.markdown }
|
9
|
+
s.files = %w{ CHANGES.markdown init.rb lib/restful/apimodel/attribute.rb lib/restful/apimodel/collection.rb lib/restful/apimodel/link.rb lib/restful/apimodel/map.rb lib/restful/apimodel/resource.rb lib/restful/converters/active_record.rb lib/restful/rails/action_controller.rb lib/restful/rails/active_record/configuration.rb lib/restful/rails/active_record/metadata_tools.rb lib/restful/rails.rb lib/restful/serializers/atom_like_serializer.rb lib/restful/serializers/base.rb lib/restful/serializers/hash_serializer.rb lib/restful/serializers/json_serializer.rb lib/restful/serializers/params_serializer.rb lib/restful/serializers/xml_serializer.rb lib/restful.rb LICENSE.markdown rails/init.rb Rakefile README.markdown restful.gemspec test/converters/active_record_converter_test.rb test/converters/basic_types_converter_test.rb test/fixtures/models/paginated_collection.rb test/fixtures/models/person.rb test/fixtures/models/pet.rb test/fixtures/models/wallet.rb test/fixtures/people.json.yaml test/fixtures/people.xml.yaml test/fixtures/pets.json.yaml test/fixtures/pets.xml.yaml test/rails/active_record_metadata_test.rb test/rails/configuration_test.rb test/rails/restful_publish_test.rb test/serializers/atom_serializer_test.rb test/serializers/json_serializer_test.rb test/serializers/params_serializer_test.rb test/serializers/xml_serializer_test.rb test/test_helper.rb TODO.markdown }
|
10
|
+
s.has_rdoc = true
|
11
|
+
s.rdoc_options = ["--line-numbers", "--inline-source"]
|
12
|
+
s.homepage = "http://github.com/M4SSIVE/restful"
|
13
|
+
s.require_paths = %w{ lib }
|
14
|
+
s.requirements = %w{ brianmario-yajl-ruby }
|
15
|
+
s.rubygems_version = "1.3.1"
|
16
|
+
s.summary = "api niceness. "
|
17
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper.rb'
|
2
|
+
|
3
|
+
#
|
4
|
+
# FIXME: remove xml serialzation here and test resource directly.
|
5
|
+
#
|
6
|
+
context "active record converter" do
|
7
|
+
setup do
|
8
|
+
Person.restful_publish(:name, :wallet, :current_location, :pets => [:name, :species])
|
9
|
+
Pet.restful_publish(:person_id, :name) # person_id gets converted to a link automagically.
|
10
|
+
|
11
|
+
@person = Person.create(:name => "Joe Bloggs", :current_location => "Under a tree", :birthday => "1976-04-03")
|
12
|
+
@wallet = @person.wallet = Wallet.create!(:contents => "something in the wallet")
|
13
|
+
@pet = @person.pets.create(:name => "Mietze", :species => "cat")
|
14
|
+
end
|
15
|
+
|
16
|
+
teardown do
|
17
|
+
reset_config
|
18
|
+
end
|
19
|
+
|
20
|
+
specify "should be able to force expansion. force expanded attributes can never be collapsed. " do
|
21
|
+
Wallet.restful_publish(:contents)
|
22
|
+
Person.restful_publish(:name, :wallet, :current_location, { :pets => [:name, :species], :restful_options => { :force_expand => :wallet } })
|
23
|
+
Pet.restful_publish(:owner, :name)
|
24
|
+
|
25
|
+
@pet.to_restful
|
26
|
+
end
|
27
|
+
|
28
|
+
specify "should return link attributes from a model" do
|
29
|
+
@pet.to_restful.links.map { |node| node.name }.sort.should.equal [:person_id]
|
30
|
+
end
|
31
|
+
|
32
|
+
specify "should convert a NULL inner association such as person.wallet to a link with a null value" do
|
33
|
+
@person.wallet = nil
|
34
|
+
|
35
|
+
wallet = @person.to_restful(:restful_options => { :expansion => :collapsed }).links.select { |link| link.name == "wallet-restful-url" }.first
|
36
|
+
wallet.should.not.== nil
|
37
|
+
wallet.value.should.== nil
|
38
|
+
end
|
39
|
+
|
40
|
+
specify "should return plain attributes from a model" do
|
41
|
+
@pet.to_restful.simple_attributes.map { |node| node.name }.should.equal [:name]
|
42
|
+
end
|
43
|
+
|
44
|
+
specify "should return collections attributes from a model" do
|
45
|
+
restful = @person.to_restful
|
46
|
+
restful.collections.map { |node| node.name }.sort.should.equal [:pets]
|
47
|
+
end
|
48
|
+
|
49
|
+
specify "should set correct type for date" do
|
50
|
+
restful = @person.to_restful :birthday
|
51
|
+
restful.simple_attributes.detect { |node| node.name == :birthday }.extended_type.should.== :date
|
52
|
+
end
|
53
|
+
|
54
|
+
specify "should be able to convert themselves to an apimodel containing all and only the attributes exposed by Model.publish_api" do
|
55
|
+
resource = @person.to_restful
|
56
|
+
|
57
|
+
resource.simple_attributes.select { |node| node.name == :name }.should.not.blank
|
58
|
+
resource.simple_attributes.select { |node| node.name == :biography }.should.blank
|
59
|
+
|
60
|
+
mietze = @person.to_restful.collections .select { |node| node.name == :pets }.first.value.first
|
61
|
+
mietze.simple_attributes.size.should.== 2
|
62
|
+
mietze.simple_attributes.select { |node| node.name == :name }.should.not.blank
|
63
|
+
mietze.simple_attributes.select { |node| node.name == :species }.should.not.blank
|
64
|
+
end
|
65
|
+
|
66
|
+
specify "should be able to convert themselves to an apimodel containing all and only the attributes exposed by Model.publish_api. this holds true if to_restful is called with some configuration options. " do
|
67
|
+
resource = @person.to_restful(:restful_options => { :nested => false })
|
68
|
+
resource.simple_attributes.select { |node| node.name == :name }.should.not.blank
|
69
|
+
resource.simple_attributes.select { |node| node.name == :biography }.should.blank
|
70
|
+
|
71
|
+
mietze = resource.collections .select { |node| node.name == :pets }.first.value.first
|
72
|
+
mietze.simple_attributes.size.should.== 2
|
73
|
+
mietze.simple_attributes.select { |node| node.name == :name }.should.not.blank
|
74
|
+
mietze.simple_attributes.select { |node| node.name == :species }.should.not.blank
|
75
|
+
end
|
76
|
+
|
77
|
+
specify "should be able to override to_restful published fields by passing them into the method" do
|
78
|
+
api = @person.to_restful(:pets)
|
79
|
+
|
80
|
+
api.simple_attributes.should.blank?
|
81
|
+
api.collections.map { |node| node.name }.sort.should.equal [:pets]
|
82
|
+
end
|
83
|
+
|
84
|
+
specify "should be able to handle relations that are nil/null" do
|
85
|
+
@person.wallet = nil
|
86
|
+
@person.save!
|
87
|
+
@person.reload
|
88
|
+
|
89
|
+
assert_nothing_raised do
|
90
|
+
@person.to_restful
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
specify "should be able to expand a :belongs_to relationship" do
|
95
|
+
xml_should_eql_fixture(@pet.to_restful_xml(:owner), "pets", :nameless_pet)
|
96
|
+
end
|
97
|
+
|
98
|
+
specify "should return collapsed resources by default when :expansion => :collapsed is passed" do
|
99
|
+
Person.restful_publish(:name, :wallet, :restful_options => { :expansion => :collapsed })
|
100
|
+
xml_should_eql_fixture(@person.to_restful_xml, "people", :joe_bloggs)
|
101
|
+
end
|
102
|
+
|
103
|
+
specify "should be able to export content generated by methods that return strings" do
|
104
|
+
xml_should_eql_fixture(@person.to_restful_xml(:location_sentence), "people", :no_wallet)
|
105
|
+
end
|
106
|
+
|
107
|
+
specify "should be able to export content generated by methods (not attributes) and compute the correct style" do
|
108
|
+
xml_should_eql_fixture(@person.to_restful_xml(:oldest_pet), "people", :with_oldest_pet)
|
109
|
+
end
|
110
|
+
|
111
|
+
specify "should be able to export content generated by methods (not attributes) while filtering with a nested configuration" do
|
112
|
+
xml_should_eql_fixture(@person.to_restful_xml(:oldest_pet => [:species]), "people", :with_oldest_pet_species)
|
113
|
+
end
|
114
|
+
|
115
|
+
specify "should create element with nil='true' attribute if no relation is set" do
|
116
|
+
@person.wallet = nil
|
117
|
+
@person.save
|
118
|
+
|
119
|
+
xml_should_eql_fixture(@person.to_restful_xml(:wallet), "people", :joe_with_zwiebelleder)
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper.rb'
|
2
|
+
|
3
|
+
context "basic types converter" do
|
4
|
+
teardown { reset_config }
|
5
|
+
|
6
|
+
specify "should raise exception if not all array contents respond to .to_restful" do
|
7
|
+
Person.restful_publish(:name)
|
8
|
+
|
9
|
+
should.raise(TypeError) do
|
10
|
+
[""].to_restful
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
specify "should convert an empty array to a restful collection" do
|
15
|
+
collection = [].to_restful
|
16
|
+
collection.name.should.== "nil-classes"
|
17
|
+
end
|
18
|
+
|
19
|
+
specify "should convert an array to a restful collection" do
|
20
|
+
Person.restful_publish(:name)
|
21
|
+
|
22
|
+
collection = [Person.create(:name => "Joe Bloggs")].to_restful
|
23
|
+
collection.name.should.== "people"
|
24
|
+
collection.value.size.should.== 1
|
25
|
+
collection.value.first.simple_attributes.first.value.should.== "Joe Bloggs"
|
26
|
+
end
|
27
|
+
|
28
|
+
specify "should set total_entries on the restful collection if the array responds to this" do
|
29
|
+
Person.restful_publish(:name)
|
30
|
+
people = PaginatedCollection.new([Person.create(:name => "Joe Bloggs")])
|
31
|
+
people.total_entries = 1001
|
32
|
+
|
33
|
+
collection = people.to_restful
|
34
|
+
collection.total_entries.should.== 1001
|
35
|
+
end
|
36
|
+
|
37
|
+
specify "should be able to convert a hash to a resource map" do
|
38
|
+
Person.restful_publish(:name)
|
39
|
+
resource = { "zeperson" => @person = Person.create(:name => "fuddzle") }.to_restful
|
40
|
+
resource.should.is_a?(Restful::ApiModel::Map)
|
41
|
+
|
42
|
+
attrs = resource.simple_attributes
|
43
|
+
attrs.size.should.== 1
|
44
|
+
attrs.first.name.should.== "zeperson"
|
45
|
+
|
46
|
+
attrs.first.value.values.first.value.== "fuddzle"
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class Person < ActiveRecord::Base
|
2
|
+
has_many :pets
|
3
|
+
has_one :wallet
|
4
|
+
|
5
|
+
accepts_nested_attributes_for :pets
|
6
|
+
accepts_nested_attributes_for :wallet
|
7
|
+
|
8
|
+
def oldest_pet
|
9
|
+
pets.first :order => "age DESC"
|
10
|
+
end
|
11
|
+
|
12
|
+
def location_sentence
|
13
|
+
"Hi. I'm currently in #{ current_location }"
|
14
|
+
end
|
15
|
+
|
16
|
+
def has_pets
|
17
|
+
pets.size > 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def pets_ages_hash
|
21
|
+
returning pets_hash = {} do
|
22
|
+
pets.each do |pet|
|
23
|
+
pets_hash[pet.name] = pet.age
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
apiable
|
29
|
+
end
|