miasma 0.0.1 → 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.
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