cistern 0.0.3

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.
@@ -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: []