sunstone 0.1.0

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