miasma 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/README.md +179 -0
  4. data/lib/miasma.rb +52 -0
  5. data/lib/miasma/contrib/aws.rb +390 -0
  6. data/lib/miasma/contrib/aws/auto_scale.rb +85 -0
  7. data/lib/miasma/contrib/aws/compute.rb +112 -0
  8. data/lib/miasma/contrib/aws/load_balancer.rb +185 -0
  9. data/lib/miasma/contrib/aws/orchestration.rb +338 -0
  10. data/lib/miasma/contrib/rackspace.rb +164 -0
  11. data/lib/miasma/contrib/rackspace/auto_scale.rb +84 -0
  12. data/lib/miasma/contrib/rackspace/compute.rb +104 -0
  13. data/lib/miasma/contrib/rackspace/load_balancer.rb +117 -0
  14. data/lib/miasma/contrib/rackspace/orchestration.rb +255 -0
  15. data/lib/miasma/error.rb +89 -0
  16. data/lib/miasma/models.rb +14 -0
  17. data/lib/miasma/models/auto_scale.rb +55 -0
  18. data/lib/miasma/models/auto_scale/group.rb +64 -0
  19. data/lib/miasma/models/auto_scale/groups.rb +34 -0
  20. data/lib/miasma/models/block_storage.rb +0 -0
  21. data/lib/miasma/models/compute.rb +70 -0
  22. data/lib/miasma/models/compute/server.rb +71 -0
  23. data/lib/miasma/models/compute/servers.rb +35 -0
  24. data/lib/miasma/models/dns.rb +0 -0
  25. data/lib/miasma/models/load_balancer.rb +55 -0
  26. data/lib/miasma/models/load_balancer/balancer.rb +72 -0
  27. data/lib/miasma/models/load_balancer/balancers.rb +34 -0
  28. data/lib/miasma/models/monitoring.rb +0 -0
  29. data/lib/miasma/models/orchestration.rb +127 -0
  30. data/lib/miasma/models/orchestration/event.rb +38 -0
  31. data/lib/miasma/models/orchestration/events.rb +64 -0
  32. data/lib/miasma/models/orchestration/resource.rb +79 -0
  33. data/lib/miasma/models/orchestration/resources.rb +55 -0
  34. data/lib/miasma/models/orchestration/stack.rb +144 -0
  35. data/lib/miasma/models/orchestration/stacks.rb +46 -0
  36. data/lib/miasma/models/queues.rb +0 -0
  37. data/lib/miasma/models/storage.rb +60 -0
  38. data/lib/miasma/models/storage/bucket.rb +36 -0
  39. data/lib/miasma/models/storage/buckets.rb +41 -0
  40. data/lib/miasma/models/storage/file.rb +45 -0
  41. data/lib/miasma/models/storage/files.rb +52 -0
  42. data/lib/miasma/types.rb +13 -0
  43. data/lib/miasma/types/api.rb +145 -0
  44. data/lib/miasma/types/collection.rb +116 -0
  45. data/lib/miasma/types/data.rb +53 -0
  46. data/lib/miasma/types/model.rb +118 -0
  47. data/lib/miasma/types/thin_model.rb +76 -0
  48. data/lib/miasma/utils.rb +12 -0
  49. data/lib/miasma/utils/animal_strings.rb +29 -0
  50. data/lib/miasma/utils/immutable.rb +36 -0
  51. data/lib/miasma/utils/lazy.rb +231 -0
  52. data/lib/miasma/utils/memoization.rb +55 -0
  53. data/lib/miasma/utils/smash.rb +149 -0
  54. data/lib/miasma/version.rb +4 -0
  55. data/miasma.gemspec +18 -0
  56. metadata +57 -3
@@ -0,0 +1,53 @@
1
+ require 'miasma'
2
+
3
+ module Miasma
4
+ module Types
5
+
6
+ # Base data container
7
+ class Data
8
+
9
+ include Miasma::Utils::Lazy
10
+
11
+ attribute :id, [String, Numeric]
12
+
13
+ # Build new data instance
14
+ #
15
+ # @param args [Hash] attribute values
16
+ # @return [self]
17
+ def initialize(args={})
18
+ load_data(args)
19
+ valid_state
20
+ end
21
+
22
+ # Convert model to JSON string
23
+ #
24
+ # @return [String]
25
+ def to_json(*_)
26
+ MultiJson.dump(attributes)
27
+ end
28
+
29
+ # Load model using JSON string
30
+ #
31
+ # @param json [String]
32
+ # @return [self]
33
+ def from_json(json)
34
+ load_data(
35
+ MultiJson.load(json).to_smash
36
+ ).valid_state
37
+ end
38
+
39
+ class << self
40
+
41
+ # Build new instance from JSON string
42
+ #
43
+ # @param json [String]
44
+ # @return [Data]
45
+ def from_json(json)
46
+ self.new(MultiJson.load(json).to_smash)
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,118 @@
1
+ require 'miasma'
2
+
3
+ module Miasma
4
+ module Types
5
+
6
+ # Base model
7
+ class Model < Data
8
+
9
+ # @return [Miasma::Types::Api] underlying service API
10
+ attr_reader :api
11
+
12
+ class << self
13
+
14
+ # Build new model from JSON
15
+ #
16
+ # @param api [Miasma::Types::Api]
17
+ # @param json [String]
18
+ # @return [Model]
19
+ def from_json(api, json)
20
+ instance = self.new(api)
21
+ instance.from_json(json)
22
+ instance
23
+ end
24
+
25
+ end
26
+
27
+ # Build new model
28
+ #
29
+ # @param api [Miasma::Types::Api] service API
30
+ # @param model_data [Smash] load model data if provided
31
+ # @return [self]
32
+ def initialize(api, model_data=nil)
33
+ @api = api
34
+ @data = Smash.new
35
+ @dirty = Smash.new
36
+ if(model_data)
37
+ if(model_data.is_a?(Hash))
38
+ load_data(model_data)
39
+ else
40
+ raise TypeError.new "Expecting `model_data` to be of type `Hash`. Received: `#{model_data.class}`"
41
+ end
42
+ end
43
+ end
44
+
45
+ # Save changes to the model
46
+ #
47
+ # @return [TrueClass, FalseClass] save was performed
48
+ # @raises [Miasma::Error::Save]
49
+ def save
50
+ if(dirty?)
51
+ perform_save
52
+ reload
53
+ else
54
+ false
55
+ end
56
+ end
57
+
58
+ # Destroy the model
59
+ #
60
+ # @return [TrueClass, FalseClass] destruction was performed
61
+ # @raises [Miasma::Error::Destroy]
62
+ def destroy
63
+ if(persisted?)
64
+ perform_destroy
65
+ reload
66
+ true
67
+ else
68
+ false
69
+ end
70
+ end
71
+
72
+ # Reload the underlying data for model
73
+ #
74
+ # @return [self]
75
+ def reload
76
+ perform_reload
77
+ self
78
+ end
79
+
80
+ # @return [TrueClass, FalseClass] model is persisted
81
+ def persisted?
82
+ id?
83
+ end
84
+
85
+ # @return [String, Integer]
86
+ def id?
87
+ data[:id] || dirty[:id]
88
+ end
89
+
90
+ protected
91
+
92
+ # Save model state to remote API
93
+ #
94
+ # @return [TrueClass, FalseClass] performed remote action
95
+ # @raises [Miasma::Error::Save]
96
+ def perform_save
97
+ raise NotImplementedError.new 'Remote API save has not been implemented'
98
+ end
99
+
100
+ # Reload model state from remote API
101
+ #
102
+ # @return [TrueClass, FalseClass] performed remote action
103
+ # @raises [Miasma::Error::Save]
104
+ def perform_reload
105
+ raise NotImplementedError.new 'Remote API reload has not been implemented'
106
+ end
107
+
108
+ # Destroy model from remote API
109
+ #
110
+ # @return [TrueClass, FalseClass] performed remote action
111
+ # @raises [Miasma::Error::Save]
112
+ def perform_destroy
113
+ raise NotImplementedError.new 'Remote API destroy has not been implemented'
114
+ end
115
+
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,76 @@
1
+ require 'miasma'
2
+
3
+ module Miasma
4
+ module Types
5
+
6
+ # Base data container
7
+ class ThinModel < Data
8
+
9
+ class << self
10
+
11
+ # Get/Set fat model
12
+ #
13
+ # @param klass [Class] fat model class
14
+ # @return [Class] fat model class
15
+ def model(klass=nil)
16
+ if(klass)
17
+ unless(klass.ancestors.include?(Miasma::Types::Model))
18
+ raise TypeError.new "Expecting `Miasma::Types::Model` subclass! (got #{klass})"
19
+ else
20
+ self._model = klass
21
+ end
22
+ end
23
+ self._model
24
+ end
25
+
26
+ protected
27
+
28
+ # @return [Class] fat model class
29
+ attr_accessor :_model
30
+
31
+ end
32
+
33
+ # @return [Miasma::Types::Api] service API
34
+ attr_reader :api
35
+
36
+ # Build new instance
37
+ #
38
+ # @param api [Miasma::Types::Api] service API
39
+ # @param args [Hash] model data
40
+ def initialize(api, args={})
41
+ @api = api
42
+ super args
43
+ end
44
+
45
+ # @return [FalseClass]
46
+ # @note thin models are always false
47
+ def persisted?
48
+ false
49
+ end
50
+
51
+ # Associated model class
52
+ #
53
+ # @return [Class] of type Miasma::Types::Model
54
+ # @note will deconstruct namespace and rebuild using provider
55
+ def model
56
+ if(self.class.model)
57
+ self.class.model
58
+ else
59
+ raise NotImplementedError.new "No associated model for this thin model type (#{self.class})"
60
+ end
61
+ end
62
+
63
+ # Build fat model instance
64
+ #
65
+ # @return [Miasma::Types::Model]
66
+ def expand
67
+ inst = model.new(api)
68
+ inst.data[:id] = self.id
69
+ inst.reload
70
+ end
71
+ alias_method :instance, :expand
72
+
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,12 @@
1
+ require 'miasma'
2
+
3
+ module Miasma
4
+ module Utils
5
+ autoload :Lazy, 'miasma/utils/lazy'
6
+ autoload :Memoization, 'miasma/utils/memoization'
7
+ autoload :Immutable, 'miasma/utils/immutable'
8
+ end
9
+ end
10
+
11
+ require 'miasma/utils/animal_strings'
12
+ require 'miasma/utils/smash'
@@ -0,0 +1,29 @@
1
+ require 'miasma'
2
+
3
+ module Miasma
4
+
5
+ module Utils
6
+ # Animal stylings on strings
7
+ module AnimalStrings
8
+
9
+ # Camel case string
10
+ # @param string [String]
11
+ # @return [String]
12
+ def camel(string)
13
+ string.to_s.split('_').map{|k| "#{k.slice(0,1).upcase}#{k.slice(1,k.length)}"}.join
14
+ end
15
+
16
+ # Snake case (underscore) string
17
+ #
18
+ # @param string [String]
19
+ # @return [String]
20
+ def snake(string)
21
+ string.to_s.gsub(/([a-z])([A-Z])/, '\1_\2').gsub('-', '_').downcase
22
+ end
23
+
24
+ end
25
+
26
+ extend AnimalStrings
27
+ end
28
+
29
+ end
@@ -0,0 +1,36 @@
1
+ require 'miasma'
2
+
3
+ module Miasma
4
+
5
+ module Utils
6
+ # Make best effort to make model immutable
7
+ # @note this should be included at end of model definition
8
+ module Immutable
9
+
10
+ # Freezes underlying data hash
11
+ def frozen_valid_state(*args)
12
+ unfrozen_valid_state(*args)
13
+ data.freeze
14
+ dirty.freeze
15
+ self
16
+ end
17
+
18
+ # @raises [Error::ImmutableError]
19
+ def save
20
+ raise Error::ImmutableError.new 'Resource information cannot be mutated!'
21
+ end
22
+
23
+ class << self
24
+
25
+ def included(klass)
26
+ klass.class_eval do
27
+ alias_method :unfrozen_valid_state, :valid_state
28
+ alias_method :valid_state, :frozen_valid_state
29
+ end
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,231 @@
1
+ require 'miasma'
2
+ require 'digest/sha2'
3
+
4
+ module Miasma
5
+ module Utils
6
+ # Adds functionality to facilitate laziness
7
+ module Lazy
8
+
9
+ # Instance methods for laziness
10
+ module InstanceMethods
11
+
12
+ # @return [Smash] argument hash
13
+ def data
14
+ unless(@data)
15
+ @data = Smash.new
16
+ end
17
+ @data
18
+ end
19
+
20
+ # @return [Smash] updated data
21
+ def dirty
22
+ unless(@dirty)
23
+ @dirty = Smash.new
24
+ end
25
+ @dirty
26
+ end
27
+
28
+ # @return [Smash] current data state
29
+ def attributes
30
+ data.merge(dirty)
31
+ end
32
+
33
+ # Create new instance
34
+ #
35
+ # @param args [Hash]
36
+ # @return [self]
37
+ def load_data(args={})
38
+ args = args.to_smash
39
+ @data = Smash.new
40
+ self.class.attributes.each do |name, options|
41
+ val = args[name]
42
+ if(options[:required] && !args.has_key?(name) && !options.has_key?(:default))
43
+ raise ArgumentError.new("Missing required option: `#{name}`")
44
+ end
45
+ if(val.nil? && !args.has_key?(name) && options[:default])
46
+ if(options[:default])
47
+ val = options[:default].respond_to?(:call) ? options[:default].call : options[:default]
48
+ end
49
+ end
50
+ if(args.has_key?(name) || val)
51
+ self.send("#{name}=", val)
52
+ end
53
+ end
54
+ self
55
+ end
56
+
57
+ # Identifies valid state and automatically
58
+ # merges dirty attributes into data, clears
59
+ # dirty attributes
60
+ #
61
+ # @return [self]
62
+ def valid_state
63
+ data.merge!(dirty)
64
+ dirty.clear
65
+ @_checksum = Digest::SHA256.hexdigest(MultiJson.dump(data))
66
+ self
67
+ end
68
+
69
+ # Model is dirty or specific attribute is dirty
70
+ #
71
+ # @param attr [String, Symbol] name of attribute
72
+ # @return [TrueClass, FalseClass] model or attribute is dirty
73
+ def dirty?(attr=nil)
74
+ if(attr)
75
+ dirty.has_key?(attr)
76
+ else
77
+ if(@_checksum)
78
+ !dirty.empty? ||
79
+ @_checksum != Digest::SHA256.hexdigest(MultiJson.dump(data))
80
+ else
81
+ true
82
+ end
83
+ end
84
+ end
85
+
86
+ # @return [String]
87
+ def to_s
88
+ "<#{self.class.name}:#{object_id}>"
89
+ end
90
+
91
+ # @return [String]
92
+ def inspect
93
+ "<#{self.class.name}:#{object_id} [#{data.inspect}]>"
94
+ end
95
+
96
+ end
97
+
98
+ # Class methods for laziness
99
+ module ClassMethods
100
+
101
+ # Add new attributes to class
102
+ #
103
+ # @param name [String]
104
+ # @param type [Class, Array<Class>]
105
+ # @param options [Hash]
106
+ # @option options [TrueClass, FalseClass] :required must be provided on initialization
107
+ # @option options [Object, Proc] :default default value
108
+ # @option options [Proc] :coerce
109
+ # @return [nil]
110
+ def attribute(name, type, options={})
111
+ name = name.to_sym
112
+ options = options.to_smash
113
+ attributes[name] = Smash.new(:type => type).merge(options)
114
+ coerce = attributes[name][:coerce]
115
+ valid_types = [attributes[name][:type], NilClass].flatten.compact
116
+ allowed_values = attributes[name][:allowed]
117
+ multiple_values = attributes[name][:multiple]
118
+ depends_on = attributes[name][:depends]
119
+ define_method(name) do
120
+ send(depends_on) if depends_on
121
+ self.class.on_missing(self) unless data.has_key?(name) || dirty.has_key?(name)
122
+ dirty[name] || data[name]
123
+ end
124
+ define_method("#{name}=") do |val|
125
+ values = multiple_values && val.is_a?(Array) ? val : [val]
126
+ values.map! do |item|
127
+ valid_type = valid_types.detect do |klass|
128
+ item.is_a?(klass)
129
+ end
130
+ if(coerce && !valid_type)
131
+ item = coerce.arity == 2 ? coerce.call(item, self) : coerce.call(item)
132
+ end
133
+ valid_type = valid_types.detect do |klass|
134
+ item.is_a?(klass)
135
+ end
136
+ unless(valid_type)
137
+ raise TypeError.new("Invalid type for `#{name}` (#{item} <#{item.class}>). Valid - #{valid_types.map(&:to_s).join(',')}")
138
+ end
139
+ if(allowed_values)
140
+ unless(allowed_values.include?(item))
141
+ raise ArgumentError.new("Invalid value provided for `#{name}` (#{item.inspect}). Allowed - #{allowed_values.map(&:inspect).join(', ')}")
142
+ end
143
+ end
144
+ item
145
+ end
146
+ if(!multiple_values && !val.is_a?(Array))
147
+ dirty[name] = values.first
148
+ else
149
+ dirty[name] = values
150
+ end
151
+ end
152
+ define_method("#{name}?") do
153
+ send(depends_on) if depends_on
154
+ self.class.on_missing(self) unless data.has_key?(name)
155
+ !!data[name]
156
+ end
157
+ nil
158
+ end
159
+
160
+ # Return attributes
161
+ #
162
+ # @param args [Symbol] :required or :optional
163
+ # @return [Array<Hash>]
164
+ def attributes(*args)
165
+ @attributes ||= Smash.new
166
+ if(args.include?(:required))
167
+ Smash[@attributes.find_all{|k,v| v[:required]}]
168
+ elsif(args.include?(:optional))
169
+ Smash[@attributes.find_all{|k,v| !v[:required]}]
170
+ else
171
+ @attributes
172
+ end
173
+ end
174
+
175
+ # Instance method to call on missing attribute or
176
+ # object to call method on if set
177
+ #
178
+ # @param param [Symbol, Object]
179
+ # @return [Symbol]
180
+ def on_missing(param=nil)
181
+ if(param)
182
+ if(param.is_a?(Symbol))
183
+ @missing_method = param
184
+ else
185
+ if(@missing_method)
186
+ param.send(@missing_method)
187
+ end
188
+ @missing_method
189
+ end
190
+ else
191
+ @missing_method
192
+ end
193
+ end
194
+
195
+ # Directly set attribute hash
196
+ #
197
+ # @param attrs [Hash]
198
+ # @return [TrueClass]
199
+ # @todo need deep dup here
200
+ def set_attributes(attrs)
201
+ @attributes = attrs.to_smash
202
+ true
203
+ end
204
+
205
+ end
206
+
207
+ class << self
208
+
209
+ # Injects laziness into class
210
+ #
211
+ # @param klass [Class]
212
+ def included(klass)
213
+ klass.class_eval do
214
+ include InstanceMethods
215
+ extend ClassMethods
216
+
217
+ class << self
218
+
219
+ def inherited(klass)
220
+ klass.set_attributes(self.attributes)
221
+ end
222
+
223
+ end
224
+ end
225
+ end
226
+
227
+ end
228
+
229
+ end
230
+ end
231
+ end