api_resource 0.6.21 → 0.6.22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/Gemfile.lock +1 -1
  4. data/LICENSE.txt +22 -0
  5. data/README.md +16 -9
  6. data/docs/Attributes.md +64 -0
  7. data/docs/Caching.md +45 -0
  8. data/docs/GettingStarted.md +149 -0
  9. data/docs/Relationships.md +136 -0
  10. data/docs/ResourceDefinition.md +80 -0
  11. data/docs/Retrieval.md +279 -0
  12. data/docs/Serialization.md +56 -0
  13. data/lib/api_resource/associations/has_many_remote_object_proxy.rb +2 -2
  14. data/lib/api_resource/attributes.rb +16 -4
  15. data/lib/api_resource/base.rb +98 -19
  16. data/lib/api_resource/conditions/abstract_condition.rb +241 -129
  17. data/lib/api_resource/conditions/include_condition.rb +7 -1
  18. data/lib/api_resource/conditions/pagination_condition.rb +37 -0
  19. data/lib/api_resource/conditions/where_condition.rb +19 -0
  20. data/lib/api_resource/conditions.rb +18 -2
  21. data/lib/api_resource/connection.rb +27 -13
  22. data/lib/api_resource/exceptions.rb +11 -11
  23. data/lib/api_resource/finders/abstract_finder.rb +176 -95
  24. data/lib/api_resource/finders/multi_object_association_finder.rb +10 -9
  25. data/lib/api_resource/finders/resource_finder.rb +59 -49
  26. data/lib/api_resource/finders/single_finder.rb +5 -6
  27. data/lib/api_resource/finders/single_object_association_finder.rb +52 -51
  28. data/lib/api_resource/finders.rb +1 -1
  29. data/lib/api_resource/formats/file_upload_format.rb +75 -0
  30. data/lib/api_resource/formats.rb +4 -1
  31. data/lib/api_resource/response.rb +108 -0
  32. data/lib/api_resource/scopes.rb +62 -5
  33. data/lib/api_resource/serializer.rb +1 -1
  34. data/lib/api_resource/typecasters/boolean_typecaster.rb +1 -0
  35. data/lib/api_resource/typecasters/integer_typecaster.rb +1 -0
  36. data/lib/api_resource/typecasters/time_typecaster.rb +12 -4
  37. data/lib/api_resource/version.rb +1 -1
  38. data/lib/api_resource.rb +1 -0
  39. data/spec/lib/associations/has_one_remote_object_proxy_spec.rb +4 -4
  40. data/spec/lib/associations_spec.rb +3 -3
  41. data/spec/lib/attributes_spec.rb +16 -1
  42. data/spec/lib/base_spec.rb +121 -39
  43. data/spec/lib/conditions/{abstract_conditions_spec.rb → abstract_condition_spec.rb} +23 -11
  44. data/spec/lib/conditions/pagination_condition_spec.rb +88 -0
  45. data/spec/lib/finders/multi_object_association_finder_spec.rb +55 -27
  46. data/spec/lib/finders/resource_finder_spec.rb +26 -2
  47. data/spec/lib/finders/single_object_association_finder_spec.rb +14 -6
  48. data/spec/lib/finders_spec.rb +81 -81
  49. data/spec/lib/observing_spec.rb +3 -4
  50. data/spec/lib/response_spec.rb +18 -0
  51. data/spec/lib/scopes_spec.rb +25 -1
  52. data/spec/lib/typecasters/boolean_typecaster_spec.rb +1 -1
  53. data/spec/lib/typecasters/integer_typecaster_spec.rb +1 -1
  54. data/spec/lib/typecasters/time_typecaster_spec.rb +6 -0
  55. data/spec/support/files/bg-awesome.jpg +0 -0
  56. data/spec/support/mocks/test_resource_mocks.rb +26 -16
  57. data/spec/support/requests/test_resource_requests.rb +27 -23
  58. metadata +24 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 36c5282462a20c9ac625c83f0c8b0c4135c459de
4
- data.tar.gz: cb4fc1a93be1e17ec2e7eaa78e4e4d9206c79211
3
+ metadata.gz: 7509a2d4163d8d10f94db97575b59deec95663a2
4
+ data.tar.gz: a540de64f15a6196656ca31b43efdee89c816c66
5
5
  SHA512:
6
- metadata.gz: b051f1d5e8e30d08560ce740386431a8ce4afe39927e79e5b2a732fd562bf307f42d08589e86cdb7b32ed2850d2108965297cb657a7096f394042e45f814675e
7
- data.tar.gz: a00eb0d1a846aa56090cae8ccbae5874051647dbcb45d71428d744a6270632b7aed705648dbc82cf253b00a01708fdacf76ddc7b3c89c187ce0aaa61f87220c0
6
+ metadata.gz: e1860666ecc1aa458a8f35121c6b98017af00161f1ce418476d12c823efa77b4f5e19b24ba6c559b9855cfccbe9ec1827d37d5c3f12400a1bedbf3aadf282a20
7
+ data.tar.gz: 9d22a6386bc3f13727f7a3e2ea56112b07012874237f340b78b77d5b07a7422c009f160ecb01a0311c6ffa341ea24f8fa63f1595b2137f815d8b8a9607ab7c0a
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- api_resource (0.6.20)
4
+ api_resource (0.6.21)
5
5
  activemodel
6
6
  colorize
7
7
  differ
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Ethan Langevin
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -2,23 +2,30 @@
2
2
 
3
3
  [![Build Status](https://travis-ci.org/LifebookerInc/api_resource.png)](https://travis-ci.org/LifebookerInc/api_resource)
4
4
 
5
- ## Installation
5
+ ApiResource is an ActiveModel-compatible library for retrieving and
6
+ persisting data via APIs
6
7
 
7
- Add this line to your application's Gemfile:
8
+ ## Getting Started
8
9
 
9
- gem 'api_resource'
10
+ 1. Add this line to your application's Gemfile:
10
11
 
11
- And then execute:
12
+ gem 'api_resource'
12
13
 
13
- $ bundle
14
+ 1. And then execute:
14
15
 
15
- Or install it yourself as:
16
+ $ bundle
16
17
 
17
- $ gem install api_resource
18
+ 1. Follow our {file:docs/GettingStarted.md Getting Started Guide} to learn more about how to use ApiResource
18
19
 
19
- ## Usage
20
+ ## Read more about some of the core concepts in ApiResource
21
+
22
+ ### {file:docs/ResourceDefinition.md The Resource Definition}
23
+ ### {file:docs/Attributes.md Attributes}
24
+ ### {file:docs/Relationships.md Relationships}
25
+ ### {file:docs/Retrieval.md Retrieving Records}
26
+ ### {file:docs/Serialization.md Serialization}
27
+ ### {file:docs/Caching.md Caching}
20
28
 
21
- TODO: Write usage instructions here
22
29
 
23
30
  ## Contributing
24
31
 
@@ -0,0 +1,64 @@
1
+ # Attributes
2
+
3
+ Attributes are read from the Server application's
4
+ {file:docs/ResourceDefinition.md Resource Definition}.
5
+
6
+ ## Visibility
7
+
8
+ ### Public
9
+
10
+ Can read and write, data is sent to the server if it has changed
11
+
12
+ ### Protected
13
+
14
+ Can read only. Cannot set or mass-assign
15
+
16
+ # Resource definition
17
+ # {
18
+ # ...
19
+ # protected : [
20
+ # ['updated_at', 'time']
21
+ # ]
22
+ # ...
23
+ # }
24
+
25
+ Resource::Person.new(updated_at: Time.now) # => Raises error
26
+ resource = Resource::Person.new
27
+ resource.updated_at = Time.now #=> Raises error
28
+
29
+ ### Private
30
+
31
+ Field is not even definted or included in the
32
+ {file:docs/ResourceDefinition.md Resource Definition}. For more
33
+ info see {http://path/to/server/docs ApiResourceServer ApiResource Server}
34
+
35
+
36
+ ## Typecasting
37
+
38
+ ApiResource supports the following types:
39
+
40
+ 1. Array
41
+ 1. Boolean
42
+ 1. Date
43
+ 1. Float
44
+ 1. Integer
45
+ 1. String
46
+ 1. Time
47
+
48
+ Attributes are given a type in the
49
+ {file:docs/ResourceDefinition.md Resource Definition}. For more
50
+ info see {http://path/to/server/docs ApiResourceServer ApiResource Server}
51
+
52
+ ## Dirty Tracking
53
+
54
+ ApiResource::Base includes ActiveModel::Dirty and uses Dirty Tracking to
55
+ determine which attributes to send to the server on save. Only attributes
56
+ that have changed are sent to the server.
57
+
58
+ person = Resource::Person.find(1)
59
+ person.attributes
60
+ # => { first_name: 'Aaron', last_name: 'Burr', ...}
61
+ person.last_name = 'Copeland'
62
+
63
+ person.save
64
+ # PUT /people/1.json { person: { last_name: 'Copeland' } }
data/docs/Caching.md ADDED
@@ -0,0 +1,45 @@
1
+ # Caching
2
+
3
+ Caching is implemented at the connection level and caches the
4
+ respone body and headers of any GET request made while caching
5
+ is active for the period specified
6
+
7
+ ## Where is it cached?
8
+
9
+ By default we use Rails.cache
10
+
11
+ # Configure the cache to use a MemoryStore
12
+ ApiResource::Base.cache = ActiveSupport::Cache::MemoryStore.new
13
+
14
+
15
+ ## Activating caching globally
16
+
17
+ This will cache all GET requests for 30 seconds
18
+
19
+ ApiResource::Base.ttl = 30.seconds
20
+
21
+ ## Activating caching for a given find call
22
+
23
+ Resource::Person.born_on(Date.today).expires_in(30.seconds).all
24
+
25
+
26
+ ## Cache expiration
27
+
28
+ Resource::Person.born_on(Date.today).expires_in(30.seconds).all
29
+
30
+ # no HTTP request here
31
+ Resource::Person.born_on(Date.today).expires_in(30.seconds).all
32
+
33
+ sleep(30)
34
+
35
+ # cache has expired - new HTTP Request
36
+ Resource::Person.born_on(Date.today).expires_in(30.seconds).all
37
+
38
+
39
+ ## Differing cache times for the same URL
40
+
41
+ Cache requests are specific to the cache interval specified. For example,
42
+ if a find with a 60 second TTL and then another with a 30 second TTL will
43
+ make two calls
44
+
45
+
@@ -0,0 +1,149 @@
1
+ # Getting Started with ApiResource
2
+
3
+ ## Creating a model in your Client application
4
+
5
+ All of your models will extend {ApiResource::Base}
6
+
7
+ # This must be loaded explicitly or created in a directory that is
8
+ # autoloaded
9
+
10
+ # lib/resources/person.rb
11
+ module Resources
12
+ class Person < ApiResource::Base
13
+ end
14
+ end
15
+
16
+ ## Adding a resource definition in your Server application
17
+
18
+ On the server-side, you will need to activate the resource
19
+
20
+ # app/modeperson.rb
21
+ class Person < ActiveRecord::Base
22
+
23
+ # this gives Person access to the methods necessary
24
+ # to create a resrouce definition
25
+ include LifebookerCommon::Model::Resource
26
+
27
+ # Example attributes
28
+ #
29
+ # :first_name, :last_name, :birthday
30
+
31
+ # Example validation
32
+ #
33
+ validates :birthday,
34
+ presence: true
35
+
36
+ end
37
+
38
+ Read more about {file:docs/ResourceDefinition.md Resource Definitions}
39
+
40
+ ## Adding basic routes and controller actions to your Server application
41
+
42
+ # config/routes.rb
43
+ resources :people
44
+
45
+ # app/controllers/people_controller.rb
46
+ class PeopleController < ApplicationController
47
+
48
+ respond_to :json
49
+
50
+ # GET /people/new
51
+ def new
52
+ respond_with(Person.resource_definition)
53
+ end
54
+
55
+ # GET /people
56
+ def index
57
+ respond_with(Person.all)
58
+ end
59
+
60
+ # GET /people/:id
61
+ def show
62
+ @person = Person.find(params[:id])
63
+ respond_with(@person)
64
+ end
65
+
66
+ # POST /people
67
+ def create
68
+ @person = Person.create(params[:person])
69
+ respond_with(@person)
70
+ end
71
+
72
+ # PUT /people/:id
73
+ def update
74
+ @person = Person.find(params[:id])
75
+ @person.update_attributes(params[:person])
76
+ respond_with(@person)
77
+ end
78
+
79
+ # DELETE /people/:id
80
+ def destory
81
+ @person = Person.find(params[:id])
82
+ @person.destory
83
+ respond_with(@person)
84
+ end
85
+ end
86
+
87
+ ## Creating a new record in your Client application
88
+
89
+ ApiResource knows about your Server model's attributes through its
90
+ {file:docs/ResourceDefinition.md Resource Definition}, so you can just
91
+ set its attributes and call {#save}
92
+
93
+ It attempts to replicate the behaviors of ActiveRecord as closely as possible
94
+
95
+ # in any part of your Client application
96
+ @person = Resources::Person.new(first_name: 'Aaron', last_name: 'Burr')
97
+ @person.save #=> true/false
98
+
99
+ # if we have errors
100
+ @person.errors #=> ActiveModel::Errors
101
+ @person.errors.full_messages #=> ['Birthday is required.']
102
+
103
+ Read more about {file:docs/Persistance.md Persistance}
104
+
105
+ ## Finding a single record in your Client application
106
+
107
+ # raises ApiResource::ResourceNotFound if no Person is found on
108
+ # the server and a 404 is returned
109
+ #
110
+ # GET /people/#{params[:id]}.json
111
+ @person = Resource::Person.find(params[:id])
112
+
113
+ ## Finding multiple records in your Client application
114
+
115
+ The query interface mimics ActiveRecord/Arel and reads scopes from the
116
+ {file:docs/ResourceDefinition.md Resource Definitions}
117
+
118
+ # GET /people.json
119
+ @people = Resource::Person.all # all people
120
+
121
+ # GET /people.json?first_name=Aaron
122
+ @people = Resource::Person.where(first_name: 'Aaron')
123
+ #=> ApiResource::ScopeCondition - Loaded on demand when you start
124
+ # iterating through the resource (e.g. @people.each {...})
125
+
126
+ For more information see {file:docs/Retrieval.md#scopes Scopes}
127
+
128
+ ## Updating a record
129
+
130
+ To update, you just find one or more records and call
131
+ {ApiResource::Base#update_attributes #update_attributes} on each record
132
+
133
+ @person = Resource::Person.find(1)
134
+ @person.update_attributes(
135
+ first_name: 'Joseph',
136
+ last_name: 'Stalin',
137
+ birthday: nil
138
+ )
139
+ # => true/false
140
+
141
+ @person.errors.full_messages # => ['Birthday is required.']
142
+
143
+ ## Deleting a record
144
+
145
+ To delete, just find a record and call {ApiResource::Base#destroy #destroy}
146
+ on it
147
+
148
+ @person = Resource::Person.find(1)
149
+ @person.destroy # => true/false
@@ -0,0 +1,136 @@
1
+ # Relationships
2
+
3
+ ApiResource mimics ActiveRecord's relationships, but loads the data via an
4
+ API or instantiates a record when data is nested
5
+
6
+ ## There are 3 ways to include associated data in a response for APIResource
7
+
8
+ The proper approach will depend on the specifics of the Server and Client
9
+ applications. For example, if an associated resource is used just about
10
+ every time the parent is retrieved, it makes sense to nest it.
11
+
12
+ In general, nesting increases the overall complexity of the Server app and
13
+ slows it down though, so it should be used with caution
14
+
15
+ Models in the Server application
16
+
17
+ class Person < ActiveRecord::Base
18
+ belongs_to :state
19
+ has_many :weapons, through: :person_weapons
20
+ end
21
+
22
+ class PersonWeapon < ActiveRecord::Base
23
+ belongs_to :weapon
24
+ belongs_to :person
25
+ end
26
+
27
+ class Weapon < ActiveRecord::Base
28
+ scope :sharp, -> { where(sharp: true) }
29
+ end
30
+
31
+ class State < ActiveRecord::Base
32
+ end
33
+
34
+ The Server application would need to have Controllers defined to expose these
35
+ models and their {file:docs/ResourceDefinition.md Resource Definitions}. For
36
+ an example of that visit {file:docs/GettingStarted.md Getting Started}
37
+
38
+
39
+ 1. Include the data nested in the response
40
+
41
+ # GET /people/1.json
42
+
43
+ # Response
44
+ {
45
+ first_name: 'Aaron',
46
+ last_name: 'Burr',
47
+ weapons: [
48
+ { id: 1, name: 'Ax', sharp: true },
49
+ { id: 2, name: 'Pistol', sharp: false }
50
+ ],
51
+ state: { id: 10, name: 'New York' }
52
+ }
53
+
54
+ # in the Client application
55
+ person = Resource::Person.find(1)
56
+ person.state.name # => 'New York'
57
+ person.weapons.length # => 2
58
+
59
+ # no additional HTTP calls are made
60
+
61
+ 1. Include a link to the data in the response
62
+
63
+ # GET /people/1.json
64
+
65
+ # Response
66
+ {
67
+ first_name: 'Aaron',
68
+ last_name: 'Burr',
69
+ weapons: [ { service_uri: '/people/1/weapons' } ],
70
+ state: { service_uri: '/states/10' }
71
+ }
72
+
73
+ # in the Client application
74
+ person = Resource::Person.find(1)
75
+ person.state.name # => 'New York'
76
+ person.weapons.length # => 2
77
+
78
+ # 2 additional HTTP calls are made
79
+ # (1 to each :service_uri provided)
80
+
81
+ 1. Include the ids for the associated objects in the response
82
+
83
+ # GET /people/1.json
84
+
85
+ # Response
86
+ {
87
+ first_name: 'Aaron',
88
+ last_name: 'Burr',
89
+ weapon_ids: [ 1, 2 ],
90
+ state_id: 10
91
+ }
92
+
93
+ # in the Client application
94
+ person = Resource::Person.find(1)
95
+ person.state.name # => 'New York'
96
+ person.weapons.length # => 2
97
+
98
+ # 2 additional HTTP calls are made
99
+ # GET /weapons.json?ids[]=1&ids[]=2
100
+ # and
101
+ # GET /states/10.json
102
+
103
+
104
+ ## Applying a scope to a relationship
105
+
106
+ Any scope on the relationship model can be applied to the relationship and
107
+ will be passed on to the server
108
+
109
+ *Note:* This does not work with embedded data because it has already
110
+ be loaded with the parent resource
111
+
112
+
113
+ @person = Resource::Person.find(1)
114
+
115
+ # GET /people/1/weapons.json
116
+ @person.weapons
117
+ # => [ Resource::Weapon(id: 1, name: 'Ax', sharp: true), Resource::Weapon(id: 2, name: 'Pistol', sharp: false) ]
118
+
119
+ # GET /people/1/weapons.json?sharp=true
120
+ @person.weapons.sharp
121
+ # => [ Resource::Weapon(id: 1, name: 'Ax', sharp: true) ]
122
+
123
+ ## Saving associated records
124
+
125
+ By default, ApiResource assumes that any modifications to individual records
126
+ in an Association will be saved directly via that record
127
+
128
+ ApiResource::Base does provide the option to `:include_associations` on save
129
+ to mass-update records. This includes the data with the parent record's save
130
+ call.
131
+
132
+ @person = Resource::Person.find(1)
133
+
134
+ @person.state.name = 'I renamed New York'
135
+ @person.save(include_associations: [:state])
136
+ # PUT /people.json { state: { name: 'I renamed New York', id: 10 } }
@@ -0,0 +1,80 @@
1
+ # The Resource Definition
2
+
3
+ The Resource Definition is the way that the Server application communicates
4
+ the properties and scopes of its models to the Client application
5
+
6
+ ## How do I set this up in my Server application?
7
+
8
+ Full documentation is available at {http://path/to/server/docs ApiResourceServer}
9
+
10
+ ## Definition Components
11
+
12
+ ### Attributes
13
+
14
+ Attributes are typically fields in the database, but can be declared using
15
+ `virtual_attribute` in the Server's model as well
16
+
17
+ #### Visibility
18
+
19
+ ApiResourceServer hooks into `attr_protected` and provides `attr_private` to
20
+ communicate visibility of different attributes
21
+
22
+
23
+ ### Scopes
24
+
25
+ ApiResource also hooks into ActiveRecord's `scope` to communicate
26
+ the models in the Server applications' scopes
27
+
28
+ ### Associations
29
+
30
+ ApiResource also hooks into ActiveRecord's `has_many`, `belongs_to` and
31
+ `has_one` associations to communicate the models in the Server applications'
32
+ associations
33
+
34
+ ## Exposing the Resource Definition
35
+
36
+ The Server application is responsible for exposing the Resource Definition
37
+ to the Client application. It should do so at
38
+ `GET /PLURALIZED_RESOURCE_NAME/new.json`
39
+
40
+
41
+ ## Examples
42
+
43
+ See {http://path/to/server/docs ApiResourceServer} for more information
44
+ on declaring your attributes
45
+
46
+
47
+ ## Final Resource Definition
48
+
49
+ {
50
+ attributes: {
51
+ public: [
52
+ ["birthday", Date],
53
+ ["first_name", "string"],
54
+ ["last_name", "string"]
55
+ ],
56
+ protected: [
57
+ ["created_at", "time"],
58
+ ["id", "integer"]
59
+ ["updated_at", "time"]
60
+ ]
61
+ },
62
+ associations : {
63
+ belongs_to: {
64
+ state: {}
65
+ },
66
+ has_many: {
67
+ friends: {}
68
+ }
69
+
70
+ },
71
+ scopes: {
72
+ active: {},
73
+ born_on: { date: :req }
74
+ }
75
+
76
+ }
77
+
78
+
79
+
80
+