sunstone 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ module Sunstone
2
+
3
+ class RecordInvalid < ::Exception
4
+ end
5
+
6
+ class RecordNotSaved < ::Exception
7
+ end
8
+
9
+ class Exception < ::Exception
10
+
11
+ class UnexpectedResponse < Sunstone::Exception
12
+ end
13
+
14
+ class BadRequest < Sunstone::Exception
15
+ attr_reader :response
16
+ def initialize(response)
17
+ super
18
+ @response = response
19
+ end
20
+ end
21
+
22
+ class Unauthorized < Sunstone::Exception
23
+ end
24
+
25
+ class NotFound < Sunstone::Exception
26
+ end
27
+
28
+ class Gone < Sunstone::Exception
29
+ end
30
+
31
+ class MovedPermanently < Sunstone::Exception
32
+ end
33
+
34
+ class ApiVersionUnsupported < Sunstone::Exception
35
+ end
36
+
37
+ class ServiceUnavailable < Sunstone::Exception
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+
@@ -0,0 +1,23 @@
1
+ require 'sunstone/model/attributes'
2
+ require 'sunstone/model/associations'
3
+ require 'sunstone/model/persistence'
4
+
5
+ module Sunstone
6
+ class Model
7
+
8
+ extend ActiveModel::Naming
9
+ include ActiveModel::Conversion
10
+
11
+ include Sunstone::Model::Attributes
12
+ include Sunstone::Model::Associations
13
+ include Sunstone::Model::Persistence
14
+
15
+ def initialize(attrs={})
16
+ super
17
+ attrs.each do |k, v|
18
+ self.send(:"#{k}=", v)
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,89 @@
1
+ module Sunstone
2
+ class Model
3
+
4
+ module Associations
5
+
6
+ extend ActiveSupport::Concern
7
+
8
+ attr_accessor :assoications
9
+
10
+ def initialize(*)
11
+ super
12
+ @associations = {}
13
+ end
14
+
15
+ def reflect_on_associations
16
+ self.class.reflect_on_associations
17
+ end
18
+
19
+ module ClassMethods
20
+
21
+ def inherited(subclass)
22
+ super
23
+ subclass.initialize_associations
24
+ end
25
+
26
+ def initialize_associations
27
+ @associations = {}
28
+ end
29
+
30
+ def reflect_on_associations
31
+ @associations
32
+ end
33
+
34
+ def belongs_to(name, options = {})
35
+ @associations[name] = {
36
+ :name => name,
37
+ :macro => :belongs_to,
38
+ :klass => (options[:class_name] || name).to_s.camelize.constantize,
39
+ :foreign_key => (options[:foreign_key] || :"#{name}_id")
40
+ }
41
+
42
+ attribute(@associations[name][:foreign_key], :integer)
43
+ define_association_reader(@associations[name])
44
+ define_association_writer(@associations[name])
45
+ end
46
+
47
+ def has_many(name, options = {})
48
+ @associations[name] = {
49
+ :name => name,
50
+ :macro => :has_many,
51
+ :klass => (options[:class_name] || name.to_s.singularize).to_s.camelize.constantize
52
+ }
53
+
54
+ define_association_reader(@associations[name])
55
+ define_association_writer(@associations[name])
56
+ end
57
+
58
+ def define_association_reader(association)
59
+ # if association[:macro] == :belongs_to
60
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
61
+ def #{association[:name]}
62
+ @associations[:#{association[:name]}]
63
+ end
64
+ EOV
65
+ # end
66
+ end
67
+
68
+ def define_association_writer(association)
69
+ if association[:macro] == :belongs_to
70
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
71
+ def #{association[:name]}=(value)
72
+ self.#{association[:foreign_key]} = value.id if !value.nil?
73
+ @associations[:#{association[:name]}] = value
74
+ end
75
+ EOV
76
+ elsif association[:macro] == :has_many
77
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
78
+ def #{association[:name]}=(value)
79
+ @associations[:#{association[:name]}] = value
80
+ end
81
+ EOV
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,99 @@
1
+ module Sunstone
2
+ class Model
3
+
4
+ class SchemaDefiner
5
+
6
+ attr_reader :defined_attributes
7
+
8
+ def initialize(schema)
9
+ @schema = schema
10
+ @defined_attributes = []
11
+ end
12
+
13
+ def attribute(name, type, options = {})
14
+ @schema.attribute(name, type, options)
15
+ @defined_attributes << name
16
+ end
17
+
18
+ Sunstone::Type::Value.subclasses.each do |type|
19
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
20
+ def #{type.name.demodulize.downcase}(name, options = {})
21
+ attribute(name, "#{type.name.demodulize.downcase}", options)
22
+ end
23
+ EOV
24
+ end
25
+
26
+ end
27
+
28
+ module Attributes
29
+
30
+ extend ActiveSupport::Concern
31
+
32
+ attr_accessor :attributes
33
+
34
+ def initialize(*)
35
+ @attributes = {}
36
+ end
37
+
38
+ def schema
39
+ self.class.schema
40
+ end
41
+
42
+ def has?(attribute)
43
+ !!@attributes[attribute]
44
+ end
45
+
46
+ module ClassMethods
47
+
48
+ def inherited(subclass)
49
+ super
50
+ subclass.initialize_schema
51
+ end
52
+
53
+ def initialize_schema
54
+ @schema = Sunstone::Schema.new
55
+ attribute(:id, :integer)
56
+ end
57
+
58
+ def schema
59
+ @schema
60
+ end
61
+
62
+ def define_schema(&block)
63
+ definer = SchemaDefiner.new(@schema)
64
+ definer.instance_eval(&block)
65
+
66
+ definer.defined_attributes.each do |name|
67
+ define_attribute_reader(name, @schema[name])
68
+ define_attribute_writer(name, @schema[name])
69
+ end
70
+ end
71
+
72
+ def attribute(name, type, options = {})
73
+ attribute = @schema.attribute(name, type, options)
74
+
75
+ define_attribute_reader(name, attribute)
76
+ define_attribute_writer(name, attribute)
77
+ end
78
+
79
+ def define_attribute_reader(name, type)
80
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
81
+ def #{name}
82
+ @attributes[:#{name}]
83
+ end
84
+ EOV
85
+ end
86
+
87
+ def define_attribute_writer(name, type)
88
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
89
+ def #{name}=(value)
90
+ @attributes[:#{name}] = schema[:#{name}].type_cast_from_user(value)
91
+ end
92
+ EOV
93
+ end
94
+
95
+ end
96
+
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,168 @@
1
+ class Wankel::SaxEncoder
2
+
3
+ def value(val)
4
+ if val.is_a?(Numeric)
5
+ number(val)
6
+ elsif val.is_a?(String)
7
+ string(val)
8
+ elsif val.nil?
9
+ null
10
+ elsif val == true || val == false
11
+ boolean(val)
12
+ elsif val.is_a?(Array)
13
+ array_open
14
+ val.each {|v| value(v) }
15
+ array_close
16
+ else
17
+ puts 'fail'
18
+ end
19
+ end
20
+
21
+ end
22
+
23
+ module Sunstone
24
+ class Model
25
+
26
+ module Persistence
27
+
28
+ extend ActiveSupport::Concern
29
+
30
+ def initialize(*)
31
+ super
32
+ @new_record = true
33
+ end
34
+
35
+ # Returns true if this object hasn't been saved yet -- that is, a record
36
+ # for the object doesn't exist in the database yet; otherwise, returns false.
37
+ def new_record?
38
+ @new_record
39
+ end
40
+
41
+ # Returns true if this object has been destroyed, otherwise returns false.
42
+ def destroyed?
43
+ @destroyed
44
+ end
45
+
46
+ # Returns true if the record is persisted, i.e. it's not a new record and
47
+ # it was not destroyed, otherwise returns false.
48
+ def persisted?
49
+ !(new_record? || destroyed?)
50
+ end
51
+
52
+ def serialize(options={})
53
+ attrs = options[:only] || schema.attributes.keys
54
+
55
+ output = StringIO.new
56
+ encoder = Wankel::SaxEncoder.new(output)
57
+
58
+ encoder.map_open
59
+
60
+ attrs.each do |name|
61
+ encoder.string name
62
+ encoder.value schema[name].type_cast_for_json(self.send(name))
63
+ end
64
+
65
+ encoder.map_close
66
+ encoder.complete
67
+ output.string
68
+ end
69
+
70
+ def serialize_for_create_and_update
71
+ attrs = []
72
+ schema.attributes.each do |name, type|
73
+ attrs << name if name != "id" && !type.readonly?
74
+ end
75
+
76
+ serialize(:only => attrs)
77
+ end
78
+
79
+ # Saves the model.
80
+ #
81
+ # If the model is new a record gets created, otherwise the existing record
82
+ # gets updated.
83
+ #
84
+ # TODO:
85
+ # By default, save always run validations. If any of them fail the action
86
+ # is cancelled and +save+ returns +false+. However, if you supply
87
+ # validate: false, validations are bypassed altogether. See
88
+ # ActiveRecord::Validations for more information.
89
+ #
90
+ # TODO:
91
+ # There's a series of callbacks associated with +save+. If any of the
92
+ # <tt>before_*</tt> callbacks return +false+ the action is cancelled and
93
+ # +save+ returns +false+. See ActiveRecord::Callbacks for further
94
+ # details.
95
+ #
96
+ # Attributes marked as readonly are silently ignored if the record is
97
+ # being updated.
98
+ def save(*)
99
+ create_or_update
100
+ rescue Sunstone::RecordInvalid
101
+ false
102
+ end
103
+
104
+ # Saves the model.
105
+ #
106
+ # If the model is new a record gets created, otherwise the existing record
107
+ # gets updated.
108
+ #
109
+ # TODO:
110
+ # With <tt>save!</tt> validations always run. If any of them fail
111
+ # ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations
112
+ # for more information.
113
+ #
114
+ # TODO:
115
+ # There's a series of callbacks associated with <tt>save!</tt>. If any of
116
+ # the <tt>before_*</tt> callbacks return +false+ the action is cancelled
117
+ # and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See
118
+ # ActiveRecord::Callbacks for further details.
119
+ #
120
+ # Attributes marked as readonly are silently ignored if the record is
121
+ # being updated.
122
+ def save!(*)
123
+ create_or_update || raise(RecordNotSaved)
124
+ end
125
+
126
+ private
127
+
128
+ def create_or_update
129
+ result = new_record? ? _create_record : _update_record
130
+ result != false
131
+ end
132
+
133
+ def _create_record
134
+ begin
135
+ Sunstone.post("/#{self.class.model_name.route_key}", serialize_for_create_and_update) do |response|
136
+ Sunstone::Parser.parse(self, response)
137
+ end
138
+ @new_record = false
139
+ true
140
+ rescue Sunstone::Exception::BadRequest => e
141
+ Sunstone::Parser.parse(self, e.response)
142
+ raise Sunstone::RecordInvalid
143
+ end
144
+ end
145
+
146
+ def _update_record
147
+ Sunstone.put("/#{self.class.model_name.route_key}/#{self.to_param}", serialize_for_create_and_update) do |response|
148
+ Sunstone::Parser.parse(self, response)
149
+ end
150
+ end
151
+
152
+ module ClassMethods
153
+
154
+ def find(id)
155
+ Sunstone.get("/#{self.model_name.route_key}/#{id}") do |response|
156
+ model = Sunstone::Parser.parse(self, response)
157
+ model.instance_variable_set(:@new_record, false)
158
+ model
159
+ end
160
+ end
161
+
162
+ end
163
+
164
+
165
+ end
166
+
167
+ end
168
+ end
@@ -0,0 +1,93 @@
1
+ module Sunstone
2
+ class Parser < Wankel::SaxParser
3
+
4
+ attr_reader :object
5
+
6
+ def self.parse(instance_or_class, response_or_body)
7
+ parser = self.new(instance_or_class)
8
+
9
+ if response_or_body.is_a?(Net::HTTPResponse)
10
+ response_or_body.read_body do |chunk|
11
+ parser << chunk
12
+ end
13
+ else
14
+ parser << response_or_body
15
+ end
16
+
17
+ parser.complete
18
+ end
19
+
20
+ def initialize(klass, options=nil)
21
+ super(options)
22
+
23
+ @object = klass.class == Class ? klass.new : klass
24
+ @stack = []
25
+ end
26
+
27
+ def on_map_start
28
+ if @stack.size == 0
29
+ @stack << @object
30
+ elsif @stack.last.is_a?(Array)
31
+ key = @stack[-2].to_sym
32
+ if @stack[-3].reflect_on_associations[key]
33
+ @stack << @stack[-3].reflect_on_associations[key][:klass].new
34
+ end
35
+ else
36
+ key = @stack.last.to_sym
37
+ if @stack[-2].reflect_on_associations[key]
38
+ @stack << @stack[-2].reflect_on_associations[key][:klass].new
39
+ end
40
+ end
41
+ end
42
+
43
+ def on_map_end
44
+ value = @stack.pop
45
+
46
+ on_value(value) if @stack.size > 0
47
+
48
+ value
49
+ end
50
+
51
+ def on_map_key(key)
52
+ @stack << key
53
+ end
54
+
55
+ def on_value(value)
56
+ if @stack.last.is_a?(Array)
57
+ @stack.last << value
58
+ else
59
+ attribute = @stack.pop
60
+ @stack.last.send(:"#{attribute}=", value) if @stack.last.respond_to?(:"#{attribute}=")
61
+ end
62
+ end
63
+
64
+ def on_null; on_value(nil) ;end
65
+ alias :on_boolean :on_value
66
+ alias :on_integer :on_value
67
+ alias :on_double :on_value
68
+ alias :on_string :on_value
69
+
70
+ def on_array_start
71
+ @stack << Array.new
72
+ end
73
+
74
+ def on_array_end
75
+ value = @stack.pop
76
+ attribute = @stack.pop
77
+ @stack.last.send(:"#{attribute}=", value) if @stack.last.respond_to?(:"#{attribute}=")
78
+ end
79
+
80
+ # Override to return the account
81
+ def parse(*args, &block)
82
+ super(*args, &block)
83
+ @object
84
+ end
85
+
86
+ # Override to return the account
87
+ def complete
88
+ super
89
+ @object
90
+ end
91
+
92
+ end
93
+ end