sunstone 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +29 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +4 -0
- data/Rakefile.rb +37 -0
- data/TODO.md +89 -0
- data/lib/sunstone.rb +361 -0
- data/lib/sunstone/exception.rb +43 -0
- data/lib/sunstone/model.rb +23 -0
- data/lib/sunstone/model/associations.rb +89 -0
- data/lib/sunstone/model/attributes.rb +99 -0
- data/lib/sunstone/model/persistence.rb +168 -0
- data/lib/sunstone/parser.rb +93 -0
- data/lib/sunstone/schema.rb +38 -0
- data/lib/sunstone/type/boolean.rb +19 -0
- data/lib/sunstone/type/date_time.rb +20 -0
- data/lib/sunstone/type/decimal.rb +19 -0
- data/lib/sunstone/type/integer.rb +17 -0
- data/lib/sunstone/type/mutable.rb +16 -0
- data/lib/sunstone/type/string.rb +18 -0
- data/lib/sunstone/type/value.rb +97 -0
- data/sunstone.gemspec +34 -0
- data/test/sunstone/model/associations_test.rb +55 -0
- data/test/sunstone/model/attributes_test.rb +60 -0
- data/test/sunstone/model/persistence_test.rb +173 -0
- data/test/sunstone/model_test.rb +11 -0
- data/test/sunstone/parser_test.rb +124 -0
- data/test/sunstone/schema_test.rb +25 -0
- data/test/sunstone/type/boolean_test.rb +24 -0
- data/test/sunstone/type/date_time_test.rb +31 -0
- data/test/sunstone/type/decimal_test.rb +27 -0
- data/test/sunstone/type/integer_test.rb +29 -0
- data/test/sunstone/type/string_test.rb +54 -0
- data/test/sunstone/type/value_test.rb +27 -0
- data/test/sunstone_test.rb +302 -0
- data/test/test_helper.rb +70 -0
- metadata +318 -0
@@ -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
|