cistern 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cistern.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Josh Lane
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Cistern
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'cistern'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install cistern
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/cistern/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Josh Lane"]
6
+ gem.email = ["me@joshualane.com"]
7
+ gem.description = %q{API client framework extracted from Fog}
8
+ gem.summary = %q{API client framework}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "cistern"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Cistern::VERSION
17
+
18
+ gem.add_dependency "formatador"
19
+ end
@@ -0,0 +1,22 @@
1
+ require 'cistern/version'
2
+ require 'formatador'
3
+
4
+ module Cistern
5
+ Error = Class.new(StandardError)
6
+
7
+ require 'cistern/hash'
8
+ require 'cistern/mock'
9
+ require 'cistern/wait_for'
10
+ require 'cistern/attributes'
11
+ require 'cistern/collection'
12
+ require 'cistern/model'
13
+ require 'cistern/service'
14
+
15
+ def self.timeout=(timeout)
16
+ @timeout= timeout
17
+ end
18
+
19
+ def self.timeout
20
+ @timeout || 0
21
+ end
22
+ end
@@ -0,0 +1,203 @@
1
+ module Cistern
2
+ module Attributes
3
+ module ClassMethods
4
+
5
+ def _load(marshalled)
6
+ new(Marshal.load(marshalled))
7
+ end
8
+
9
+ def aliases
10
+ @aliases ||= {}
11
+ end
12
+
13
+ def attributes
14
+ @attributes ||= []
15
+ end
16
+
17
+ def attribute(name, options = {})
18
+ class_eval <<-EOS, __FILE__, __LINE__
19
+ def #{name}
20
+ attributes[:#{name}]
21
+ end
22
+ EOS
23
+ case options[:type]
24
+ when :boolean
25
+ class_eval <<-EOS, __FILE__, __LINE__
26
+ def #{name}=(new_#{name})
27
+ attributes[:#{name}] = case new_#{name}
28
+ when true,'true'
29
+ true
30
+ when false,'false'
31
+ false
32
+ end
33
+ end
34
+ EOS
35
+ when :float
36
+ class_eval <<-EOS, __FILE__, __LINE__
37
+ def #{name}=(new_#{name})
38
+ attributes[:#{name}] = new_#{name} && new_#{name}.to_f
39
+ end
40
+ EOS
41
+ when :integer
42
+ class_eval <<-EOS, __FILE__, __LINE__
43
+ def #{name}=(new_#{name})
44
+ attributes[:#{name}] = new_#{name} && new_#{name}.to_i
45
+ end
46
+ EOS
47
+ when :string
48
+ class_eval <<-EOS, __FILE__, __LINE__
49
+ def #{name}=(new_#{name})
50
+ attributes[:#{name}] = new_#{name} && new_#{name}.to_s
51
+ end
52
+ EOS
53
+ when :time
54
+ class_eval <<-EOS, __FILE__, __LINE__
55
+ def #{name}=(new_#{name})
56
+ attributes[:#{name}] = if new_#{name}.nil? || new_#{name} == "" || new_#{name}.is_a?(Time)
57
+ new_#{name}
58
+ else
59
+ Time.parse(new_#{name})
60
+ end
61
+ end
62
+ EOS
63
+ when :array
64
+ class_eval <<-EOS, __FILE__, __LINE__
65
+ def #{name}=(new_#{name})
66
+ attributes[:#{name}] = [*new_#{name}]
67
+ end
68
+ EOS
69
+ else
70
+ if squash = options[:squash]
71
+ class_eval <<-EOS, __FILE__, __LINE__
72
+ def #{name}=(new_data)
73
+ if new_data.is_a?(::Hash)
74
+ if new_data.has_key?(:'#{squash}')
75
+ attributes[:#{name}] = new_data[:'#{squash}']
76
+ elsif new_data.has_key?("#{squash}")
77
+ attributes[:#{name}] = new_data["#{squash}"]
78
+ else
79
+ attributes[:#{name}] = [ new_data ]
80
+ end
81
+ else
82
+ attributes[:#{name}] = new_data
83
+ end
84
+ end
85
+ EOS
86
+ else
87
+ class_eval <<-EOS, __FILE__, __LINE__
88
+ def #{name}=(new_#{name})
89
+ attributes[:#{name}] = new_#{name}
90
+ end
91
+ EOS
92
+ end
93
+ end
94
+ @attributes ||= []
95
+ @attributes |= [name]
96
+ for new_alias in [*options[:aliases]]
97
+ aliases[new_alias] = name
98
+ end
99
+ end
100
+
101
+ def identity(name, options = {})
102
+ @identity = name
103
+ self.attribute(name, options)
104
+ end
105
+
106
+ def ignore_attributes(*args)
107
+ @ignored_attributes = args
108
+ end
109
+
110
+ def ignored_attributes
111
+ @ignored_attributes ||= []
112
+ end
113
+
114
+ end
115
+
116
+ module InstanceMethods
117
+
118
+ def _dump(level)
119
+ Marshal.dump(attributes)
120
+ end
121
+
122
+ def attributes
123
+ @attributes ||= {}
124
+ end
125
+
126
+ def dup
127
+ copy = super
128
+ copy.dup_attributes!
129
+ copy
130
+ end
131
+
132
+ def identity
133
+ send(self.class.instance_variable_get('@identity'))
134
+ end
135
+
136
+ def identity=(new_identity)
137
+ send("#{self.class.instance_variable_get('@identity')}=", new_identity)
138
+ end
139
+
140
+ def merge_attributes(new_attributes = {})
141
+ for key, value in new_attributes
142
+ unless self.class.ignored_attributes.include?(key)
143
+ if aliased_key = self.class.aliases[key]
144
+ send("#{aliased_key}=", value)
145
+ elsif self.respond_to?("#{key}=",true)
146
+ send("#{key}=", value)
147
+ else
148
+ attributes[key] = value
149
+ end
150
+ end
151
+ end
152
+ self
153
+ end
154
+
155
+ def new_record?
156
+ !identity
157
+ end
158
+
159
+ # check that the attributes specified in args exist and is not nil
160
+ def requires(*args)
161
+ missing = missing_attributes(args)
162
+ if missing.length == 1
163
+ raise(ArgumentError, "#{missing.first} is required for this operation")
164
+ elsif missing.any?
165
+ raise(ArgumentError, "#{missing[0...-1].join(", ")} and #{missing[-1]} are required for this operation")
166
+ end
167
+ end
168
+
169
+ def requires_one(*args)
170
+ missing = missing_attributes(args)
171
+ if missing.length == args.length
172
+ raise(ArgumentError, "#{missing[0...-1].join(", ")} or #{missing[-1]} are required for this operation")
173
+ end
174
+ end
175
+
176
+ protected
177
+
178
+ def missing_attributes(args)
179
+ missing = []
180
+ for arg in [:connection] | args
181
+ unless send("#{arg}") || attributes.has_key?(arg)
182
+ missing << arg
183
+ end
184
+ end
185
+ missing
186
+ end
187
+
188
+ def dup_attributes!
189
+ @attributes = @attributes.dup
190
+ end
191
+
192
+ private
193
+
194
+ def remap_attributes(attributes, mapping)
195
+ for key, value in mapping
196
+ if attributes.key?(key)
197
+ attributes[value] = attributes.delete(key)
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,122 @@
1
+ class Cistern::Collection < Array
2
+ extend Cistern::Attributes::ClassMethods
3
+ include Cistern::Attributes::InstanceMethods
4
+
5
+ Array.public_instance_methods(false).each do |method|
6
+ unless [:reject, :select, :slice].include?(method.to_sym)
7
+ class_eval <<-EOS, __FILE__, __LINE__
8
+ def #{method}(*args)
9
+ unless @loaded
10
+ lazy_load
11
+ end
12
+ super
13
+ end
14
+ EOS
15
+ end
16
+ end
17
+
18
+ %w[reject select slice].each do |method|
19
+ class_eval <<-EOS, __FILE__, __LINE__
20
+ def #{method}(*args)
21
+ unless @loaded
22
+ lazy_load
23
+ end
24
+ data = super
25
+ self.clone.clear.concat(data)
26
+ end
27
+ EOS
28
+ end
29
+
30
+ def self.model(new_model=nil)
31
+ if new_model == nil
32
+ @model
33
+ else
34
+ @model = new_model
35
+ end
36
+ end
37
+
38
+ def initialize(attributes = {})
39
+ @loaded = false
40
+ merge_attributes(attributes)
41
+ end
42
+
43
+ def create(attributes={})
44
+ model = self.new(attributes)
45
+ model.save
46
+ end
47
+
48
+ def get(identity)
49
+ raise NotImplementedError
50
+ end
51
+
52
+ def clear
53
+ @loaded = true
54
+ super
55
+ end
56
+
57
+ def model
58
+ self.class.instance_variable_get('@model')
59
+ end
60
+
61
+ attr_accessor :connection
62
+
63
+ def new(attributes = {})
64
+ unless attributes.is_a?(::Hash)
65
+ raise(ArgumentError.new("Initialization parameters must be an attributes hash, got #{attributes.class} #{attributes.inspect}"))
66
+ end
67
+ model.new(
68
+ attributes.merge(
69
+ :collection => self,
70
+ :connection => connection
71
+ )
72
+ )
73
+ end
74
+
75
+ def load(objects)
76
+ clear
77
+ for object in objects
78
+ self << new(object)
79
+ end
80
+ self
81
+ end
82
+
83
+ def inspect
84
+ Thread.current[:formatador] ||= Formatador.new
85
+ data = "#{Thread.current[:formatador].indentation}<#{self.class.name}\n"
86
+ Thread.current[:formatador].indent do
87
+ unless self.class.attributes.empty?
88
+ data << "#{Thread.current[:formatador].indentation}"
89
+ data << self.class.attributes.map {|attribute| "#{attribute}=#{send(attribute).inspect}"}.join(",\n#{Thread.current[:formatador].indentation}")
90
+ data << "\n"
91
+ end
92
+ data << "#{Thread.current[:formatador].indentation}["
93
+ unless self.empty?
94
+ data << "\n"
95
+ Thread.current[:formatador].indent do
96
+ data << self.map {|member| member.inspect}.join(",\n")
97
+ data << "\n"
98
+ end
99
+ data << Thread.current[:formatador].indentation
100
+ end
101
+ data << "]\n"
102
+ end
103
+ data << "#{Thread.current[:formatador].indentation}>"
104
+ data
105
+ end
106
+
107
+ def reload
108
+ clear
109
+ lazy_load
110
+ self
111
+ end
112
+
113
+ def table(attributes = nil)
114
+ Formatador.display_table(self.map {|instance| instance.attributes}, attributes)
115
+ end
116
+
117
+ private
118
+
119
+ def lazy_load
120
+ self.all
121
+ end
122
+ end
@@ -0,0 +1,7 @@
1
+ class Cistern::Hash
2
+ def self.slice(hash, *keys)
3
+ {}.tap do |sliced|
4
+ keys.each{|k| sliced[k]= hash[k] if hash.key?(k)}
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ module Cistern
2
+ class Mock
3
+ def self.not_implemented
4
+ raise NotImplementedError
5
+ end
6
+
7
+ def self.random_hex(length)
8
+ rand(('f' * length).to_i(16)).to_s(16).rjust(length, '0')
9
+ end
10
+
11
+ def self.random_numbers(length)
12
+ rand(('9' * length).to_i).to_s
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,61 @@
1
+ class Cistern::Model
2
+ extend Cistern::Attributes::ClassMethods
3
+ include Cistern::Attributes::InstanceMethods
4
+
5
+ attr_accessor :collection, :connection
6
+
7
+ def initialize(attributes={})
8
+ merge_attributes(attributes)
9
+ end
10
+
11
+ def inspect
12
+ Thread.current[:formatador] ||= Formatador.new
13
+ data = "#{Thread.current[:formatador].indentation}<#{self.class.name}"
14
+ Thread.current[:formatador].indent do
15
+ unless self.class.attributes.empty?
16
+ data << "\n#{Thread.current[:formatador].indentation}"
17
+ data << self.class.attributes.map {|attribute| "#{attribute}=#{send(attribute).inspect}"}.join(",\n#{Thread.current[:formatador].indentation}")
18
+ end
19
+ end
20
+ data << "\n#{Thread.current[:formatador].indentation}>"
21
+ data
22
+ end
23
+
24
+
25
+ def save
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def reload
30
+ requires :identity
31
+
32
+ if data = collection.get(identity)
33
+ new_attributes = data.attributes
34
+ merge_attributes(new_attributes)
35
+ self
36
+ end
37
+ end
38
+
39
+ def ==(comparison_object)
40
+ comparison_object.equal?(self) ||
41
+ (comparison_object.is_a?(self.class) &&
42
+ comparison_object.identity == self.identity &&
43
+ !comparison_object.new_record?)
44
+ end
45
+
46
+ def wait_for(timeout=Cistern.timeout, interval=1, &block)
47
+ reload
48
+ retries = 3
49
+ Cistern.wait_for(timeout, interval) do
50
+ if reload
51
+ retries = 3
52
+ elsif retries > 0
53
+ retries -= 1
54
+ sleep(1)
55
+ elsif retries == 0
56
+ raise Cistern::Error.new("Reload failed, #{self.class} #{self.identity} went away.") # FIXME: pretty much assumes you are calling #ready?
57
+ end
58
+ instance_eval(&block)
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,148 @@
1
+ class Cistern::Service
2
+ def self.mock!; @mocking= true; end
3
+ def self.mocking?; @mocking; end
4
+ def self.unmock!; @mocking= false; end
5
+ module Collections
6
+
7
+ def collections
8
+ service.collections
9
+ end
10
+
11
+ def mocked_requests
12
+ service.mocked_requests
13
+ end
14
+
15
+ def requests
16
+ service.requests
17
+ end
18
+
19
+ end
20
+ class << self
21
+ def inherited(klass)
22
+ klass.class_eval <<-EOS, __FILE__, __LINE__
23
+ module Collections
24
+ include Cistern::Service::Collections
25
+
26
+ def service
27
+ #{klass.name}
28
+ end
29
+ end
30
+ def self.service
31
+ #{klass.name}
32
+ end
33
+ EOS
34
+ end
35
+ def model_path(model_path)
36
+ @model_path = model_path
37
+ end
38
+
39
+ def request_path(request_path)
40
+ @request_path = request_path
41
+ end
42
+
43
+ def collections
44
+ @collections ||= []
45
+ end
46
+
47
+ def models
48
+ @models ||= []
49
+ end
50
+
51
+ def recognized_arguments
52
+ @recognized_arguments ||= []
53
+ end
54
+
55
+ def required_arguments
56
+ @required_arguments ||= []
57
+ end
58
+
59
+ def requests
60
+ @requests ||= []
61
+ end
62
+
63
+ def requires(*args)
64
+ self.required_arguments.concat(args)
65
+ end
66
+
67
+ def recognizes(*args)
68
+ self.recognized_arguments.concat(args)
69
+ end
70
+
71
+ def model(model_name)
72
+ models << model_name
73
+ end
74
+
75
+ def mocked_requests
76
+ @mocked_requests ||= []
77
+ end
78
+
79
+ def request(request_name)
80
+ requests << request_name
81
+ end
82
+
83
+ def collection(collection_name)
84
+ collections << collection_name
85
+ end
86
+
87
+ def validate_options(options={})
88
+ required_options = Cistern::Hash.slice(options, *required_arguments)
89
+ missing_required_options = required_arguments - required_options.keys
90
+ unless missing_required_options.empty?
91
+ raise "Missing required options: #{missing_required_options.inspect}"
92
+ end
93
+ recognized_options = Cistern::Hash.slice(options, *(required_arguments + recognized_arguments))
94
+ unrecognized_options = options.keys - (required_arguments + recognized_arguments)
95
+ unless unrecognized_options.empty?
96
+ raise "Unrecognized options: #{unrecognized_options.inspect}"
97
+ end
98
+ end
99
+
100
+ def setup_requirements
101
+ @required ||= false
102
+ unless @required
103
+ models.each do |model|
104
+ require File.join(@model_path, model.to_s)
105
+ end
106
+ requests.each do |request|
107
+ require File.join(@request_path, request.to_s)
108
+ if service::Mock.method_defined?(request)
109
+ mocked_requests << request
110
+ else
111
+ service::Mock.module_eval <<-EOS, __FILE__, __LINE__
112
+ def #{request}(*args)
113
+ Cistern::Mock.not_implemented
114
+ end
115
+ EOS
116
+ end
117
+ end
118
+ collections.each do |collection|
119
+ require File.join(@model_path, collection.to_s)
120
+ class_name = collection.to_s.split("_").map(&:capitalize).join
121
+ self.const_get(:Collections).module_eval <<-EOS, __FILE__, __LINE__
122
+ def #{collection}(attributes={})
123
+ #{service}::#{class_name}.new({connection: self}.merge(attributes))
124
+ end
125
+ EOS
126
+ end
127
+ @required = true
128
+ end
129
+ end
130
+
131
+ def new(options={})
132
+ validate_options(options)
133
+ setup_requirements
134
+
135
+ if self.mocking?
136
+ self.const_get(:Mock).send(:include, self.const_get(:Collections))
137
+ self.const_get(:Mock).new(options)
138
+ else
139
+ self.const_get(:Real).send(:include, self.const_get(:Collections))
140
+ self.const_get(:Real).new(options)
141
+ end
142
+ end
143
+
144
+ def reset!
145
+ self.const_get(:Mock).reset!
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,3 @@
1
+ module Cistern
2
+ VERSION = "0.0.3"
3
+ end
@@ -0,0 +1,15 @@
1
+ module Cistern
2
+ def self.wait_for(timeout=Cistern.timeout, interval=1, &block)
3
+ duration = 0
4
+ start = Time.now
5
+ until yield || duration > timeout
6
+ sleep(interval.to_f)
7
+ duration = Time.now - start
8
+ end
9
+ if duration > timeout
10
+ false
11
+ else
12
+ { :duration => duration }
13
+ end
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cistern
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Josh Lane
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: formatador
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: API client framework extracted from Fog
31
+ email:
32
+ - me@joshualane.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - .gitignore
38
+ - Gemfile
39
+ - LICENSE
40
+ - README.md
41
+ - Rakefile
42
+ - cistern.gemspec
43
+ - lib/cistern.rb
44
+ - lib/cistern/attributes.rb
45
+ - lib/cistern/collection.rb
46
+ - lib/cistern/hash.rb
47
+ - lib/cistern/mock.rb
48
+ - lib/cistern/model.rb
49
+ - lib/cistern/service.rb
50
+ - lib/cistern/version.rb
51
+ - lib/cistern/wait_for.rb
52
+ homepage: ''
53
+ licenses: []
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project:
72
+ rubygems_version: 1.8.24
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: API client framework
76
+ test_files: []