restly 0.0.1.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +29 -0
  5. data/Rakefile +1 -0
  6. data/lib/restly/associations/base.rb +35 -0
  7. data/lib/restly/associations/belongs_to.rb +15 -0
  8. data/lib/restly/associations/builder.rb +30 -0
  9. data/lib/restly/associations/embeddable_resources/embeds_many.rb +7 -0
  10. data/lib/restly/associations/embeddable_resources/embeds_one.rb +7 -0
  11. data/lib/restly/associations/embeddable_resources.rb +28 -0
  12. data/lib/restly/associations/has_many.rb +23 -0
  13. data/lib/restly/associations/has_one.rb +23 -0
  14. data/lib/restly/associations.rb +90 -0
  15. data/lib/restly/base/fields.rb +79 -0
  16. data/lib/restly/base/generic_methods.rb +24 -0
  17. data/lib/restly/base/includes.rb +53 -0
  18. data/lib/restly/base/instance/actions.rb +35 -0
  19. data/lib/restly/base/instance/attributes.rb +88 -0
  20. data/lib/restly/base/instance/persistence.rb +20 -0
  21. data/lib/restly/base/instance.rb +76 -0
  22. data/lib/restly/base/mass_assignment_security.rb +19 -0
  23. data/lib/restly/base/resource/finders.rb +32 -0
  24. data/lib/restly/base/resource.rb +19 -0
  25. data/lib/restly/base/write_callbacks.rb +37 -0
  26. data/lib/restly/base.rb +82 -0
  27. data/lib/restly/client.rb +34 -0
  28. data/lib/restly/collection/pagination.rb +36 -0
  29. data/lib/restly/collection.rb +47 -0
  30. data/lib/restly/configuration.rb +43 -0
  31. data/lib/restly/connection.rb +99 -0
  32. data/lib/restly/controller_methods.rb +16 -0
  33. data/lib/restly/error.rb +30 -0
  34. data/lib/restly/middleware.rb +15 -0
  35. data/lib/restly/nested_attributes.rb +97 -0
  36. data/lib/restly/proxies/associations/collection.rb +22 -0
  37. data/lib/restly/proxies/associations/instance.rb +11 -0
  38. data/lib/restly/proxies/auth.rb +9 -0
  39. data/lib/restly/proxies/base.rb +39 -0
  40. data/lib/restly/proxies/params.rb +8 -0
  41. data/lib/restly/proxies.rb +18 -0
  42. data/lib/restly/railtie.rb +9 -0
  43. data/lib/restly/thread_local.rb +33 -0
  44. data/lib/restly/version.rb +3 -0
  45. data/lib/restly.rb +24 -0
  46. data/restly.gemspec +28 -0
  47. data/spec/fakewebs.rb +0 -0
  48. data/spec/helper.rb +31 -0
  49. data/spec/restly/base_spec.rb +60 -0
  50. metadata +210 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .idea/
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in restly.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Jason Waldrip
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 ADDED
@@ -0,0 +1,29 @@
1
+ # Restly
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'restly'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install restly
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,35 @@
1
+ class Restly::Associations::Base
2
+
3
+ attr_reader :name, :association_class, :namespace, :polymorphic, :options
4
+
5
+ def initialize(owner, name, options={})
6
+ @name = name
7
+ @namespace = options.delete(:namespace) || owner.name.gsub(/::\w+$/, '')
8
+ @polymorphic = options.delete(:polymorphic)
9
+ options[:class_name] ||= name.to_s.classify
10
+ @owner = owner
11
+ @association_class = [@namespace, options.delete(:class_name)].compact.join('::').constantize
12
+ @options = options
13
+ end
14
+
15
+ def collection?
16
+ false
17
+ end
18
+
19
+ def build(*args)
20
+ new_instance = @association_class.new(*args)
21
+ new_instance.write_attribute("#{@owner.resource_name}_id") if @association_class.respond_to?("#{@owner.resource_name}_id") && !self.class.send(:reflect_on_resource_association, :custom_pages).embedded?
22
+ new_instance
23
+ end
24
+
25
+ private
26
+
27
+ def authorize(klass, authorization = nil)
28
+ if (!klass.authorized? && @owner.respond_to?(:authorized?) && @owner.authorized?) || authorization
29
+ klass.authorize(authorization || @owner.connection)
30
+ else
31
+ klass
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,15 @@
1
+ class Restly::Associations::BelongsTo < Restly::Associations::Base
2
+
3
+ def find_with_parent(parent, options)
4
+ options.reverse_merge!(self.options)
5
+ association_class = polymorphic ? [@namespace, instance.send("#{name}_type")] : self.association_class
6
+ association_class = authorize(association_class, options[:authorize])
7
+ instance = association_class.find(parent.attributes["#{name}_id"])
8
+ Restly::Proxies::Associations::Instance.new(instance, parent)
9
+ end
10
+
11
+ def collection?
12
+ false
13
+ end
14
+
15
+ end
@@ -0,0 +1,30 @@
1
+ module Restly::Associations::Builder
2
+
3
+ def build(relationship, opts)
4
+
5
+ # Base Model
6
+ model = opts[:class_name] || relationship.to_s.singularize.camelize
7
+
8
+ # Namespace
9
+ namespace = opts[:namespace] || self.class.name.gsub(/::\w+$/, '')
10
+ model = [namespace, model].compact.join('::')
11
+
12
+ # Polymorphic Relationships
13
+ if opts[:polymorphic]
14
+ polymorphic_type = send(:"#{resource}_type")
15
+ model = [namespace, polymorphic_type].compact.join('::')
16
+ end
17
+
18
+ # Constantize
19
+ model = model.constantize
20
+
21
+ # Auto-authorization, fail with error!
22
+ if (!model.authorized? && self.respond_to?(:authorized?) && self.authorized?) || (opts[:authorize])
23
+ model.authorize(opts[:authorize] || self.connection)
24
+ else
25
+ model
26
+ end
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,7 @@
1
+ class Restly::Associations::EmbeddableResources::EmbedsMany < Restly::Associations::Base
2
+
3
+ def collection?
4
+ true
5
+ end
6
+
7
+ end
@@ -0,0 +1,7 @@
1
+ class Restly::Associations::EmbeddableResources::EmbedsOne < Restly::Associations::Base
2
+
3
+ def collection?
4
+ false
5
+ end
6
+
7
+ end
@@ -0,0 +1,28 @@
1
+ module Restly::Associations::EmbeddableResources
2
+ extend ActiveSupport::Autoload
3
+
4
+ autoload :EmbeddedIn
5
+ autoload :EmbedsMany
6
+ autoload :EmbedsOne
7
+
8
+ # Embeds One
9
+ def embeds_resource(name, options = {})
10
+ exclude_field(name) if ancestors.include?(Restly::Base)
11
+ self.resource_associations[name] = association = EmbedsOne.new(self, name, options)
12
+
13
+ define_method name do
14
+ association.build(attributes[name])
15
+ end
16
+ end
17
+
18
+ # Embeds Many
19
+ def embeds_resources(name, options = {})
20
+ exclude_field(name) if ancestors.include?(Restly::Base)
21
+ self.resource_associations[name] = association = EmbedsMany.new(self, name, options)
22
+
23
+ define_method name do
24
+ ( self.attributes[name] || [] ).map!{ |i| association.build(i) }
25
+ end
26
+ end
27
+
28
+ end
@@ -0,0 +1,23 @@
1
+ class Restly::Associations::HasMany < Restly::Associations::Base
2
+
3
+ attr_reader :joiner
4
+
5
+ def initialize(owner, name, options={})
6
+ @joiner = options.delete(:through)
7
+ super
8
+ end
9
+
10
+ def scope_with_parent(parent, options)
11
+ options.reverse_merge!(self.options)
12
+ association_class = polymorphic ? [@namespace, instance.send("#{name}_type")] : self.association_class
13
+ association_class = authorize(association_class, options[:authorize])
14
+ collection = association_class.with_params("with_#{parent.resource_name}_id" => parent.id).all # (parent.attributes["#{name}_id"])
15
+ collection.select{|i| i.attributes["#{parent.resource_name}_id"] == parent.id }
16
+ Restly::Proxies::Associations::Collection.new(collection, parent)
17
+ end
18
+
19
+ def collection?
20
+ true
21
+ end
22
+
23
+ end
@@ -0,0 +1,23 @@
1
+ class Restly::Associations::HasOne < Restly::Associations::Base
2
+
3
+ attr_reader :joiner
4
+
5
+ def initialize(owner, name, options={})
6
+ @joiner = options.delete(:through)
7
+ super
8
+ end
9
+
10
+ def scope_with_parent(parent, options)
11
+ options.reverse_merge!(self.options)
12
+ association_class = polymorphic ? [@namespace, instance.send("#{name}_type")] : self.association_class
13
+ association_class = authorize(association_class, options[:authorize])
14
+ collection = association_class.with_params("with_#{parent.resource_name}_id" => parent.id).all(parent.attributes["#{name}_id"])
15
+ collection.select{|i| i.attributes["#{parent.resource_name}_id"] == parent.id }.first
16
+ Restly::Proxies::Associations::Instance.new(instance, parent)
17
+ end
18
+
19
+ def collection?
20
+ true
21
+ end
22
+
23
+ end
@@ -0,0 +1,90 @@
1
+ module Restly::Associations
2
+ extend ActiveSupport::Concern
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Base
6
+ autoload :BelongsTo
7
+ autoload :HasMany
8
+ autoload :HasManyThrough
9
+ autoload :HasOne
10
+ autoload :HasOneThrough
11
+ autoload :EmbeddableResources
12
+
13
+ class AssociationHash < HashWithIndifferentAccess
14
+ end
15
+
16
+ included do
17
+
18
+ extend EmbeddableResources if self == Restly::Base
19
+ include Restly::NestedAttributes
20
+
21
+ delegate :resource_name, to: :klass
22
+ class_attribute :resource_associations, instance_reader: false, instance_writer: false
23
+ self.resource_associations = AssociationHash.new
24
+
25
+ inherited do
26
+ self.resource_associations = resource_associations.dup
27
+ end
28
+
29
+ end
30
+
31
+ def stub_associations_from_response(response=self.response)
32
+ parsed = response.parsed || {}
33
+ parsed = parsed[resource_name] if parsed.is_a?(Hash) && parsed[resource_name]
34
+ associations = parsed.select{ |i| klass.reflect_on_all_resource_associations.keys.include?(i) }
35
+ associations.each do |relationship|
36
+ relationship.collection?
37
+ end
38
+ end
39
+
40
+ def klass
41
+ self.class
42
+ end
43
+
44
+ module ClassMethods
45
+
46
+ def resource_name
47
+ name.gsub(/.*::/,'').underscore
48
+ end
49
+
50
+ def reflect_on_all_resource_associations
51
+ resource_associations
52
+ end
53
+
54
+ private
55
+
56
+ # Belongs to
57
+ def belongs_to_resource(name, options = {})
58
+ exclude_field(name) if ancestors.include?(Restly::Base)
59
+ self.resource_associations[name] = association = BelongsTo.new(self, name, options)
60
+ define_method name do |options={}|
61
+ association.find_with_parent(self, options)
62
+ end
63
+ end
64
+
65
+ # Has One
66
+ def has_one_resource(name, options = {})
67
+ exclude_field(name) if ancestors.include?(Restly::Base)
68
+ self.resource_associations[name] = association = HasOne.new(self, name, options)
69
+ define_method name do |options={}|
70
+ association.scope_with_parent(self, options)
71
+ end
72
+ end
73
+
74
+ # Has One
75
+ def has_many_resources(name, options = {})
76
+ exclude_field(name) if ancestors.include?(Restly::Base)
77
+ self.resource_associations[name] = association = HasMany.new(self, name, options)
78
+ define_method name do |options={}|
79
+ association.scope_with_parent(self, options)
80
+ end
81
+ end
82
+
83
+
84
+ def reflect_on_resource_association(association_name)
85
+ reflect_on_all_resource_associations[association_name]
86
+ end
87
+
88
+ end
89
+
90
+ end
@@ -0,0 +1,79 @@
1
+ module Restly::Base::Fields
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ extend SharedMethods
6
+ include SharedMethods
7
+ extend ClassMethods
8
+
9
+ class_attribute :fields
10
+ self.fields = FieldSet.new
11
+ field :id
12
+
13
+ inherited do
14
+ self.fields = fields.dup
15
+ end
16
+
17
+ end
18
+
19
+ module SharedMethods
20
+
21
+ def set_field(attr)
22
+ base = self.is_a?(Class) ? self : singleton_class
23
+ unless base.send :instance_method_already_implemented?, attr
24
+ base.send :define_attribute_method, attr
25
+ self.fields += [attr]
26
+ end
27
+ end
28
+
29
+ end
30
+
31
+ module ClassMethods
32
+
33
+ def field(attr)
34
+ if attr.is_a?(Hash) && attr[:from_spec]
35
+ before_initialize do
36
+ spec[:attributes].each{ |attr| set_field(attr) }
37
+ end
38
+ elsif attr.is_a?(Symbol) || attr.is_a?(String)
39
+ set_field(attr)
40
+ else
41
+ raise Restly::Error::InvalidField, "field must be a symbol or string."
42
+ end
43
+ end
44
+
45
+ def exclude_field(name)
46
+
47
+ # Remove from the class
48
+ self.fields -= [name]
49
+
50
+ # Remove from the instance
51
+ before_initialize do
52
+ self.fields -= [name]
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+
59
+ class FieldSet < Set
60
+
61
+ def include?(value)
62
+ super(value.to_sym)
63
+ end
64
+
65
+ def <<(value)
66
+ super(value.to_sym)
67
+ end
68
+
69
+ def +(other_arry)
70
+ super(other_arry.map(&:to_sym))
71
+ end
72
+
73
+ def -(other_arry)
74
+ super(other_arry.map(&:to_sym))
75
+ end
76
+
77
+ end
78
+
79
+ end
@@ -0,0 +1,24 @@
1
+ module Restly::Base::GenericMethods
2
+
3
+ def authorize(token_object)
4
+ Restly::Proxies::Auth.new self, token_object
5
+ end
6
+
7
+ def with_params(params={})
8
+ Restly::Proxies::Params.new self, params
9
+ end
10
+
11
+ def path_with_format(*args)
12
+ path = [self.path, args].flatten.compact.join('/')
13
+ [path, format].compact.join('.') if path
14
+ end
15
+
16
+ def format
17
+ client.format
18
+ end
19
+
20
+ def authorized?
21
+ connection.token.present?
22
+ end
23
+
24
+ end
@@ -0,0 +1,53 @@
1
+ module Restly::Base::Includes
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ extend ClassMethods
6
+
7
+ class_attribute :inherited_callbacks
8
+ self.inherited_callbacks = []
9
+
10
+ inherited do
11
+ self.inherited_callbacks = inherited_callbacks
12
+ end
13
+
14
+ end
15
+
16
+ module ClassMethods
17
+
18
+ # Delegate stuff to client
19
+ delegate :site, :site=, :format, :format=, to: :client
20
+
21
+ def client
22
+ @client ||= Restly::Client.new
23
+ end
24
+
25
+ def connection
26
+ connection = @connection || Restly::Connection.tokenize(client, current_token)
27
+ connection.cache ||= cache
28
+ connection.cache_options ||= cache_options
29
+ connection
30
+ end
31
+
32
+ def connection=(connection)
33
+ raise InvalidConnection, "#{connection} is not a valid Restly::Connection" unless connection.is_a?(Restly::Connection)
34
+ @connection = connection
35
+ end
36
+
37
+ def param_key
38
+ resource_name
39
+ end
40
+
41
+ private
42
+
43
+ def inherited(subclass = nil, &block)
44
+ self.inherited_callbacks << block and return if block_given?
45
+
46
+ inherited_callbacks.each do |call_block|
47
+ subclass.class_eval(&call_block)
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,35 @@
1
+ module Restly::Base::Instance::Actions
2
+
3
+ def save
4
+ run_callbacks :save do
5
+ @previously_changed = changes
6
+ @changed_attributes.clear
7
+ new_record? ? create : update
8
+ end
9
+ self
10
+ end
11
+
12
+ def delete
13
+ run_callbacks :delete do
14
+ response = connection.delete(path_with_format, params: params)
15
+ false
16
+ freeze
17
+ end
18
+ response.status < 300
19
+ end
20
+
21
+ private
22
+
23
+ def update
24
+ run_callbacks :update do
25
+ set_attributes_from_response(connection.put path_with_format, body: @request_body, params: params)
26
+ end
27
+ end
28
+
29
+ def create
30
+ run_callbacks :create do
31
+ set_attributes_from_response(connection.post path_with_format, body: @request_body, params: params)
32
+ end
33
+ end
34
+
35
+ end
@@ -0,0 +1,88 @@
1
+ module Restly::Base::Instance::Attributes
2
+
3
+ def update_attributes(attributes)
4
+ self.attributes = attributes
5
+ save
6
+ end
7
+
8
+ def attributes=(attributes)
9
+ attributes.each do |k, v|
10
+ write_attribute k, v
11
+ end
12
+ end
13
+
14
+ def attributes
15
+ nil_values = fields.inject({}) do |hash, key|
16
+ hash[key] = nil
17
+ hash
18
+ end
19
+ @attributes.reverse_merge!(nil_values)
20
+ end
21
+
22
+ def write_attribute(attr, val)
23
+ if fields.include?(attr)
24
+ send("#{attr}_will_change!".to_sym) unless val == @attributes[attr.to_sym] || !@loaded
25
+ @attributes[attr.to_sym] = val
26
+ else
27
+ puts "WARNING: Attribute `#{attr}` not written. ".colorize(:yellow) +
28
+ "To fix this add the following the the model. -- field :#{attr}"
29
+ end
30
+ end
31
+
32
+ def read_attribute(attr)
33
+ raise NoMethodError, "undefined method #{attr} for #{klass}" unless fields.include?(attr)
34
+ attributes[attr.to_sym]
35
+ end
36
+
37
+ alias :attribute :read_attribute
38
+
39
+ def inspect
40
+ inspection = if @attributes
41
+ fields.collect { |name|
42
+ "#{name}: #{attribute_for_inspect(name)}"
43
+ }.compact.join(", ")
44
+ else
45
+ "not initialized"
46
+ end
47
+ "#<#{self.class} #{inspection}>"
48
+ end
49
+
50
+ def has_attribute?(attr)
51
+ attribute(attr)
52
+ end
53
+
54
+ private
55
+
56
+ def attribute_for_inspect(attr_name)
57
+ value = attribute(attr_name)
58
+ if value.is_a?(String) && value.length > 50
59
+ "#{value[0..50]}...".inspect
60
+ else
61
+ value.inspect
62
+ end
63
+ end
64
+
65
+ def set_attributes_from_response(response=self.response)
66
+ parsed = response.parsed || {}
67
+ parsed = parsed[resource_name] if parsed.is_a?(Hash) && parsed[resource_name]
68
+ self.attributes = parsed
69
+ end
70
+
71
+ def method_missing(m, *args, &block)
72
+ if !!(/(?<attr>\w+)(?<setter>=)?$/ =~ m.to_s) && fields.include?(m)
73
+ case !!setter
74
+ when true
75
+ write_attribute(m, *args)
76
+ when false
77
+ read_attribute(m)
78
+ end
79
+ else
80
+ super(m, *args, &block)
81
+ end
82
+ end
83
+
84
+ def respond_to_missing?(method_name, include_private = false)
85
+ !!(/(?<attr>\w+)=?$/ =~ method_name.to_s) && fields.include?(method_name)
86
+ end
87
+
88
+ end
@@ -0,0 +1,20 @@
1
+ module Restly::Base::Instance::Persistence
2
+
3
+ def exists?
4
+ status = @response.try(:status).to_i
5
+ status < 300 && status >= 200
6
+ end
7
+
8
+ def persisted?
9
+ exists? && !changed?
10
+ end
11
+
12
+ def new_record?
13
+ !exists?
14
+ end
15
+
16
+ def reload!
17
+ self.class.send :instance_from_response, connection.get(path, force: true)
18
+ end
19
+
20
+ end