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