api_resource 0.6.21 → 0.6.22

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.
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
+