restly 0.0.1.alpha.1

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 (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