obvious 0.0.7 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4bf4de686eee48c524a3037620e21a154db8c75fce3ff8db780afc25dc376202
4
+ data.tar.gz: c4930e55f1420a9b40996fc042052f267548b3f6017055ad69355c44bad28f7e
5
+ SHA512:
6
+ metadata.gz: 377a83eba55eeadeb63bdadd1a12f72a81507b5d029486add3ffba65fd81c4de929936e435bc73ac0816185bab6dff2a0aa1b4ecfcf527fa36d4dbdc13abaf81
7
+ data.tar.gz: 6f7a51c4037baa95be253c62a801f1c8204e662b089bc45bd7f6db1df94f6c39d1130625fb745a7c558111662c5baeaf59372817a077f17d14faf7af2ce0b394
@@ -0,0 +1,19 @@
1
+ name: obvious
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ unit-tests:
7
+ strategy:
8
+ matrix:
9
+ ruby: [ 2.7, '3.0', 3.1 ]
10
+ name: Ruby ${{ matrix.ruby }} unit tests
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v2
14
+ - uses: ruby/setup-ruby@v1
15
+ with:
16
+ ruby-version: ${{ matrix.ruby }}
17
+ bundler-cache: true
18
+ - run: |
19
+ bundle exec rspec
data/.gitignore CHANGED
@@ -1,9 +1,9 @@
1
1
  *.gem
2
2
  *.rbc
3
+ *.swp
3
4
  .bundle
4
5
  .config
5
6
  .yardoc
6
- Gemfile.lock
7
7
  InstalledFiles
8
8
  _yardoc
9
9
  coverage
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ obvious (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.5.0)
10
+ rspec (3.10.0)
11
+ rspec-core (~> 3.10.0)
12
+ rspec-expectations (~> 3.10.0)
13
+ rspec-mocks (~> 3.10.0)
14
+ rspec-core (3.10.1)
15
+ rspec-support (~> 3.10.0)
16
+ rspec-expectations (3.10.1)
17
+ diff-lcs (>= 1.2.0, < 2.0)
18
+ rspec-support (~> 3.10.0)
19
+ rspec-mocks (3.10.2)
20
+ diff-lcs (>= 1.2.0, < 2.0)
21
+ rspec-support (~> 3.10.0)
22
+ rspec-support (3.10.3)
23
+
24
+ PLATFORMS
25
+ ruby
26
+ x86_64-darwin-20
27
+ x86_64-linux
28
+
29
+ DEPENDENCIES
30
+ obvious!
31
+ rspec
32
+
33
+ BUNDLED WITH
34
+ 2.3.4
data/README.md CHANGED
@@ -6,6 +6,16 @@ independent of the app logic itself.
6
6
 
7
7
  You can get a full explanation of Obvious at http://obvious.retromocha.com
8
8
 
9
+
10
+ # Notice:
11
+
12
+ This project is no longer under active development is only made available for historical purposes.
13
+
14
+ Right now [Brian is working on a little of this](http://brianknapp.me/now/) and [Shawn is working on a little of that](http://shawnbaden.com/now/).
15
+
16
+ Most of our spare energy goes to the [Unbranded Pocket Notebook](https://www.amazon.com/dp/B00ZGE1914/) and the [Unbranded Pocket Journal](https://www.amazon.com/dp/B016LB2XYW).
17
+
18
+
9
19
  ## Installation
10
20
 
11
21
  Add this line to your application's Gemfile:
@@ -35,11 +45,3 @@ in the Obvious Status example app: https://github.com/RetroMocha/obvious_status.
35
45
  Currently the footprint of the Obvious library is quite small. The most important things defined so far are the Contract class
36
46
  and the Hash.has_shape? method. The rest of what makes an Obvious app interesting is the structure itself, not the libraries Obvious
37
47
  provides.
38
-
39
- ## Contributing
40
-
41
- 1. Fork it
42
- 2. Create your feature branch (`git checkout -b my-new-feature`)
43
- 3. Commit your changes (`git commit -am 'Add some feature'`)
44
- 4. Push to the branch (`git push origin my-new-feature`)
45
- 5. Create new Pull Request
@@ -30,7 +30,8 @@ module Obvious
30
30
 
31
31
  puts 'Creating actions from descriptors... ' unless descriptors.length.zero?
32
32
  descriptors.each do |file|
33
- descriptor = Obvious::Generators::Descriptor.new file
33
+ yaml = YAML.load_file(file)
34
+ descriptor = Obvious::Generators::Descriptor.new yaml
34
35
  descriptor.to_file
35
36
  end
36
37
 
@@ -145,27 +146,27 @@ end
145
146
 
146
147
  output = %Q{require 'obvious'
147
148
 
148
- class #{k}Contract < Contract
149
+ class #{k}Contract < Obvious::Contract
149
150
  #{contract_defs}
150
151
  end
151
152
  }
152
153
 
153
- snake_name = name.gsub(/(.)([A-Z])/,'\1_\2').downcase
154
+ snake_name = k.gsub(/(.)([A-Z])/,'\1_\2').downcase
154
155
 
155
- filename = "#{@app.dir}/contracts/#{snake_name}_jack_contract.rb"
156
+ filename = "#{@app.dir}/contracts/#{snake_name}_contract.rb"
156
157
  File.open(filename, 'w') {|f| f.write(output) }
157
158
 
158
- output = %Q{require_relative '../../contracts/#{snake_name}_jack_contract'
159
+ output = %Q{require_relative '../../contracts/#{snake_name}_contract'
159
160
 
160
161
  describe #{k}Contract do
161
162
  #{method_specs}
162
163
  end
163
164
  }
164
165
 
165
- filename = "#{@app.dir}/spec/contracts/#{snake_name}_jack_spec.rb"
166
+ filename = "#{@app.dir}/spec/contracts/#{snake_name}_spec.rb"
166
167
  File.open(filename, 'w') {|f| f.write(output) }
167
168
 
168
- output = %Q{require_relative '../../contracts/#{snake_name}_jack_contract'
169
+ output = %Q{require_relative '../../contracts/#{snake_name}_contract'
169
170
 
170
171
  class #{k}Double
171
172
  def self.create behavior
@@ -187,7 +188,7 @@ class #{k}_BadOutput < #{k}Contract
187
188
  end
188
189
  }
189
190
 
190
- filename = "#{@app.dir}/spec/doubles/#{snake_name}_jack_double.rb"
191
+ filename = "#{@app.dir}/spec/doubles/#{snake_name}_double.rb"
191
192
  File.open(filename, 'w') {|f| f.write(output) }
192
193
 
193
194
  end
@@ -4,22 +4,25 @@ require_relative 'helpers/application'
4
4
 
5
5
  module Obvious
6
6
  module Generators
7
+ class InvalidDescriptorError < StandardError; end
8
+
7
9
  class Descriptor
8
10
  def initialize descriptor
9
11
  @descriptor = descriptor
10
12
  end
11
13
 
12
14
  def to_file
13
- action = YAML.load_file @descriptor
15
+ validate_descriptor
16
+
14
17
  @jacks, @entities = {}, {}
15
18
  @code = ''
16
19
 
17
- action['Code'].each do |entry|
20
+ @descriptor['Code'].each do |entry|
18
21
  write_comments_for entry
19
22
  process_requirements_for entry if entry['requires']
20
23
  end
21
24
 
22
- write_action action
25
+ write_action
23
26
  end
24
27
 
25
28
  private
@@ -53,12 +56,12 @@ module Obvious
53
56
  end
54
57
  end # #process_requirements_for
55
58
 
56
- def write_action action
59
+ def write_action
57
60
  jacks_data = process_jacks
58
61
  requirements = require_entities
59
62
 
60
63
  output = %Q{#{requirements}
61
- class #{action['Action']}
64
+ class #{@descriptor['Action']}
62
65
 
63
66
  def initialize #{jacks_data[:inputs]}
64
67
  #{jacks_data[:assignments]} end
@@ -68,24 +71,24 @@ class #{action['Action']}
68
71
  end
69
72
  }
70
73
 
71
- snake_name = action['Action'].gsub(/(.)([A-Z])/,'\1_\2').downcase
74
+ snake_name = @descriptor['Action'].gsub(/(.)([A-Z])/,'\1_\2').downcase
72
75
 
73
76
  filename = "#{Obvious::Generators::Application.instance.dir}/actions/#{snake_name}.rb"
74
77
  File.open(filename, 'w') {|f| f.write(output) }
75
78
 
76
- output = %Q{require_relative '../../actions/#{snake_name}'
79
+ output = %Q{require_relative '../../actions/#{snake_name}'
77
80
 
78
- describe #{action['Action']} do
81
+ describe #{@descriptor['Action']} do
79
82
 
80
- it '#{action['Description']}'
83
+ it '#{@descriptor['Description']}'
81
84
 
82
85
  it 'should raise an error with invalid input'
83
86
 
84
87
  end
85
- }
88
+ }
86
89
 
87
- filename = "#{Obvious::Generators::Application.instance.dir}/spec/actions/#{snake_name}_spec.rb"
88
- File.open(filename, 'w') {|f| f.write(output) }
90
+ filename = "#{Obvious::Generators::Application.instance.dir}/spec/actions/#{snake_name}_spec.rb"
91
+ File.open(filename, 'w') {|f| f.write(output) }
89
92
  end
90
93
 
91
94
  def process_jacks
@@ -116,6 +119,13 @@ end
116
119
 
117
120
  entity_requires
118
121
  end
122
+
123
+ def validate_descriptor
124
+ raise InvalidDescriptorError unless @descriptor
125
+ raise InvalidDescriptorError if @descriptor['Code'].nil?
126
+ raise InvalidDescriptorError if @descriptor['Action'].nil?
127
+ raise InvalidDescriptorError if @descriptor['Description'].nil?
128
+ end
119
129
  end # ::Descriptor
120
130
  end
121
131
  end
@@ -1,150 +1,161 @@
1
- class Contract
2
- @@disable_override = false
1
+ module Obvious
3
2
 
4
- # Provides a default empty array for method_added
5
- # Overriden by self.contracts
6
- def self.contract_list
7
- []
8
- end
9
-
10
- # Public: Sets the contracts
11
- #
12
- # contracts - a list of contracts, as String or as Symbols.
13
- #
14
- # Examples
15
- #
16
- # class FooJackContract < Contract
17
- # contracts :save, :get, :list
18
- #
19
- # end
20
- #
21
- # Returns Nothing.
22
- def self.contracts *contracts
23
- singleton_class.send :define_method, :contract_list do
24
- contracts
25
- end
26
- end
3
+ class Contract
4
+ @@disable_override = false
27
5
 
28
- # Public: Defines a contract for a method
29
- #
30
- # method - a symbol representing the method name
31
- # contract - a hash with the keys :input and :output holding the respective shapes.
32
- #
33
- # Examples
34
- #
35
- # class FooJackContract < Contract
36
- #
37
- # contract_for :save, {
38
- # :input => Foo.shape,
39
- # :output => Foo.shape,
40
- # }
41
- #
42
- # end
43
- #
44
- def self.contract_for method, contract
45
- method_alias = "#{method}_alias".to_sym
46
- method_contract = "#{method}_contract".to_sym
47
-
48
- define_method method_contract do |*args|
49
- input = args[0]
50
- call_method method_alias, input, contract[:input], contract[:output]
6
+ # Provides a default empty array for method_added
7
+ # Overriden by self.contracts
8
+ def self.contract_list
9
+ []
51
10
  end
52
11
 
53
- contracts( *contract_list, method )
54
- end
55
-
56
- # This method will move methods defined in @contracts into new methods.
57
- # Each entry in @contracts will cause the method with the same name to
58
- # become method_name_alias and for the original method to point to
59
- # method_name_contract.
60
- def self.method_added name
61
- unless @@disable_override
62
- self.contract_list.each do |method|
63
- if name == method.to_sym
64
- method_alias = "#{method}_alias".to_sym
65
- method_contract = "#{method}_contract".to_sym
66
-
67
- @@disable_override = true # to stop the new build method
68
- self.send :alias_method, method_alias, name
69
- self.send :remove_method, name
70
- self.send :alias_method, name, method_contract
71
-
72
- @@disable_override = false
73
- else
74
- # puts self.inspect
75
- # puts "defining other method #{name}"
76
- end
12
+ # Public: Sets the contracts
13
+ #
14
+ # contracts - a list of contracts, as String or as Symbols.
15
+ #
16
+ # Examples
17
+ #
18
+ # class FooJackContract < Contract
19
+ # contracts :save, :get, :list
20
+ #
21
+ # end
22
+ #
23
+ # Returns Nothing.
24
+ def self.contracts *contracts
25
+ singleton_class.send :define_method, :contract_list do
26
+ contracts
77
27
  end
78
28
  end
79
- end
80
29
 
81
- # This method is used as a shorthand to mak the contract method calling pattern more DRY
82
- # It starts by checking if you are sending in input and if so will check the input shape for
83
- # errors. If no errors are found it calls the method via the passed in symbol(method).
84
- #
85
- # Output checking is more complicated because of the types of output we check for. Nil is
86
- # never valid output. If we pass in the output shape of true, that means we are looking for
87
- # result to be the object True. If the output shape is an array, that is actually a shorthand
88
- # for telling our output check to look at the output as an array and compare it to the shape
89
- # stored in output_shape[0]. If we pass in the symbol :true_false it means we are looking for
90
- # the result to be either true or false. The default case will just check if result has the shape
91
- # of the output_shape.
92
- def call_method method, input, input_shape, output_shape
93
- if input != nil && input_shape != nil
94
- unless input.has_shape? input_shape
95
- raise ContractInputError, 'incorrect input data format'
30
+ # Public: Defines a contract for a method
31
+ #
32
+ # method - a symbol representing the method name
33
+ # contract - a hash with the keys :input and :output holding the respective shapes.
34
+ #
35
+ # Examples
36
+ #
37
+ # class FooJackContract < Contract
38
+ #
39
+ # contract_for :save, {
40
+ # :input => Foo.shape,
41
+ # :output => Foo.shape,
42
+ # }
43
+ #
44
+ # end
45
+ #
46
+ def self.contract_for method, contract
47
+ method_alias = "#{method}_alias".to_sym
48
+ method_contract = "#{method}_contract".to_sym
49
+
50
+ define_method method_contract do |*args|
51
+ input = args[0]
52
+ call_method method_alias, input, contract[:input], contract[:output]
96
53
  end
97
54
 
98
- result = self.send method, input
99
- else
100
- result = self.send method
55
+ contracts( *contract_list, method )
101
56
  end
102
57
 
103
- # check output
104
- # output should never be nil
105
- if result == nil
106
- raise ContractOutputError, 'incorrect output data format'
58
+ # This method will move methods defined in @contracts into new methods.
59
+ # Each entry in @contracts will cause the method with the same name to
60
+ # become method_name_alias and for the original method to point to
61
+ # method_name_contract.
62
+ def self.method_added name
63
+ unless @@disable_override
64
+ self.contract_list.each do |method|
65
+ if name == method.to_sym
66
+ method_alias = "#{method}_alias".to_sym
67
+ method_contract = "#{method}_contract".to_sym
68
+
69
+ @@disable_override = true # to stop the new build method
70
+ self.send :alias_method, method_alias, name
71
+ self.send :remove_method, name
72
+ self.send :alias_method, name, method_contract
73
+
74
+ @@disable_override = false
75
+ else
76
+ # puts self.inspect
77
+ # puts "defining other method #{name}"
78
+ end
79
+ end
80
+ end
107
81
  end
108
82
 
109
- # we are looking for result to be a True object
110
- if output_shape === true
111
- if output_shape == result
112
- return result
83
+ # This method is used as a shorthand to mak the contract method calling pattern more DRY
84
+ # It starts by checking if you are sending in input and if so will check the input shape for
85
+ # errors. If no errors are found it calls the method via the passed in symbol(method).
86
+ #
87
+ # Output checking is more complicated because of the types of output we check for. Nil is
88
+ # never valid output. If we pass in the output shape of true, that means we are looking for
89
+ # result to be the object True. If the output shape is an array, that is actually a shorthand
90
+ # for telling our output check to look at the output as an array and compare it to the shape
91
+ # stored in output_shape[0]. If we pass in the symbol :true_false it means we are looking for
92
+ # the result to be either true or false. The default case will just check if result has the shape
93
+ # of the output_shape.
94
+ def call_method method, input, input_shape, output_shape
95
+ if input != nil && input_shape != nil
96
+ has_shape, error_field = input.has_shape? input_shape, true
97
+ unless has_shape
98
+ raise ContractInputError, "incorrect input data format field #{error_field}"
99
+ end
100
+
101
+ result = self.send method, input
113
102
  else
103
+ result = self.send method
104
+ end
105
+
106
+ # check output
107
+ # output should never be nil
108
+ if result == nil
114
109
  raise ContractOutputError, 'incorrect output data format'
115
110
  end
116
- end
117
111
 
118
- # we want to check the shape of each item in the result array
119
- if output_shape.class == Array
120
- if result.class == Array
121
- inner_shape = output_shape[0]
122
- result.each do |item|
123
- unless item.has_shape? inner_shape
124
- raise ContractOutputError, 'incorrect output data format'
112
+ if result === {}
113
+ raise DataNotFoundError, 'data was not found'
114
+ end
115
+
116
+ # we are looking for result to be a True object
117
+ if output_shape === true
118
+ if output_shape == result
119
+ return result
120
+ else
121
+ raise ContractOutputError, 'incorrect output data format'
122
+ end
123
+ end
124
+
125
+ # we want to check the shape of each item in the result array
126
+ if output_shape.class == Array
127
+ if result.class == Array
128
+ inner_shape = output_shape[0]
129
+ result.each do |item|
130
+ has_shape, error_field = item.has_shape? inner_shape, true
131
+ unless has_shape
132
+ raise ContractOutputError, "incorrect output data format field #{error_field}"
133
+ end
125
134
  end
135
+
136
+ return result
137
+ end
138
+ raise ContractOutputError, 'incorrect output data format'
139
+ end
140
+
141
+ # we want result to be true or false
142
+ if output_shape == :true_false
143
+ unless result == true || result == false
144
+ raise ContractOutputError, 'incorrect output data format'
126
145
  end
127
146
 
128
147
  return result
129
148
  end
130
- raise ContractOutputError, 'incorrect output data format'
131
- end
132
149
 
133
- # we want result to be true or false
134
- if output_shape == :true_false
135
- unless result == true || result == false
136
- raise ContractOutputError, 'incorrect output data format'
150
+ # we want result to be output_shape's shape
151
+ has_shape, error_field = result.has_shape? output_shape, true
152
+ unless has_shape
153
+ raise ContractOutputError, "incorrect output data format field #{error_field}"
137
154
  end
138
155
 
139
- return result
140
- end
141
-
142
- # we want result to be output_shape's shape
143
- unless result.has_shape? output_shape
144
- raise ContractOutputError, 'incorrect output data format'
156
+ result
145
157
  end
146
158
 
147
- result
148
159
  end
149
160
 
150
161
  end
@@ -153,7 +164,7 @@ end
153
164
  class Hash
154
165
  # Checks if a hash has a certain structure.
155
166
  # h = { k1: 1, k2: "1" }
156
- # h.has_shape?(k1: Fixnum, k2: String)
167
+ # h.has_shape?(k1: Integer, k2: String)
157
168
  # #=> true
158
169
  # h.has_shape?(k1: Class, k2: String)
159
170
  # #=> false
@@ -162,17 +173,47 @@ class Hash
162
173
  # shape = { k1: Array, k2: { k3: Module } }
163
174
  # h.has_shape?(shape)
164
175
  # #=> true
165
- def has_shape?(shape)
176
+ def has_shape?(shape, return_field = false)
177
+ return_value = lambda { |r, f|
178
+ if return_field
179
+ return r, f
180
+ else
181
+ return r
182
+ end
183
+ }
184
+
166
185
  # I added an empty check
167
186
  if self.empty?
168
- return shape.empty?
169
- end
170
-
171
- shape.all? do |k, v|
187
+ return return_value.call shape.empty?, nil
188
+ end
189
+
190
+ self.each do |k, v|
191
+ return return_value.call false, k if shape[k] == nil
192
+ end
193
+
194
+ shape.each do |k, v|
172
195
  # hash_value
173
196
  hv = self[k]
174
- Hash === hv ? hv.has_shape?(v) : v === hv
197
+ return return_value.call false, k unless self.has_key? k
198
+
199
+ next if hv === nil
200
+
201
+ if Hash === hv
202
+ return hv.has_shape?(v, return_field)
203
+ else
204
+ return return_value.call false, k unless v === hv
205
+ end
175
206
  end
207
+
208
+ return_value.call true, nil
209
+ end
210
+
211
+ def nil_fields? list
212
+ list.each do |field|
213
+ return true, field unless self[field]
214
+ end
215
+
216
+ return false, nil
176
217
  end
177
218
  end
178
219
 
@@ -181,3 +222,7 @@ end
181
222
 
182
223
  class ContractOutputError < StandardError
183
224
  end
225
+
226
+ class DataNotFoundError < StandardError
227
+ end
228
+
@@ -0,0 +1,74 @@
1
+
2
+ module Obvious
3
+
4
+ module EntityMixin
5
+ class << self
6
+ def included(base)
7
+ base.extend ClassMethods
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ attr_reader :shape
13
+ attr_reader :validations
14
+
15
+ def value name, type
16
+ name = name.to_sym
17
+ @shape ||= {}
18
+ @shape[name] = type
19
+ define_method(name) { @values[name] }
20
+ end
21
+
22
+ def validation name, method
23
+ name = "#{name}_validation".to_sym
24
+ @validations ||= []
25
+ @validations << name
26
+ define_method(name) { instance_exec &method }
27
+ end
28
+ end
29
+ end
30
+
31
+ class ShapeError < StandardError; end
32
+ class TypeError < StandardError; end
33
+ class ValidationError < StandardError; end
34
+
35
+ class Entity
36
+ include EntityMixin
37
+
38
+ def initialize input
39
+ shape = self.class.shape
40
+ validations = self.class.validations || []
41
+
42
+ self.class.shape.freeze
43
+ self.class.validations.freeze
44
+
45
+ @values = {}
46
+ input.each do |k, v|
47
+ unless shape[k]
48
+ raise Obvious::ShapeError.new "Invalid input field: #{k}"
49
+ else
50
+ @values[k] = v
51
+ end
52
+ end
53
+
54
+ @values.freeze # this might need to be recursive?
55
+
56
+ shape.each do |k, v|
57
+ unless @values[k].class == v
58
+ msg = "Validation Error: Invalid value for #{k}, should be a #{v}"
59
+ raise Obvious::TypeError.new msg
60
+ end
61
+ end
62
+
63
+ validations.each { |method| send method }
64
+
65
+ freeze
66
+ end
67
+
68
+ def to_hash
69
+ {}.tap {|h| @values.each { |k, v| h[k] = v } }
70
+ end
71
+
72
+ end
73
+ end
74
+
@@ -6,13 +6,19 @@ class FsPlug
6
6
  @filename = filename
7
7
  end
8
8
 
9
- def save input
10
- # open the file
9
+ def load_data
11
10
  contents = File.read @filename
11
+ JSON.parse contents, :symbolize_names => true
12
+ end
12
13
 
14
+ def save_data input
15
+ json_data = JSON.pretty_generate input
16
+ File.open(@filename, 'w') {|f| f.write(json_data) }
17
+ end
18
+
19
+ def save input
13
20
  data = []
14
- # parse the json list
15
- query = JSON.parse contents, :symbolize_names => true
21
+ query = load_data
16
22
 
17
23
  new_element = true if input[:id] == -1 # by convention set new element flag if id == -1
18
24
 
@@ -27,51 +33,37 @@ class FsPlug
27
33
  end
28
34
 
29
35
  # add data to the list if it's a new element
30
- input[:id] = max_id + 1
31
- data << input if new_element
36
+ if new_element
37
+ input[:id] = max_id + 1
38
+ data << input
39
+ end
32
40
 
33
- # save the data back to FS
34
- json_data = JSON.pretty_generate data
35
- File.open(@filename, 'w') {|f| f.write(json_data) }
41
+ save_data data
36
42
 
37
43
  # return the transformed data
38
44
  input
39
45
  end
40
46
 
41
47
  def list
42
- # open the file
43
- contents = File.read @filename
44
-
45
- # parse the json list
46
- data = JSON.parse contents, :symbolize_names => true
47
-
48
- # return the transformed data
49
- data
48
+ load_data
50
49
  end
51
50
 
52
51
  def get input
53
- # open the file
54
- contents = File.read @filename
55
-
56
52
  data = []
57
- # parse the json list
58
- query = JSON.parse contents, :symbolize_names => true
53
+ query = load_data
59
54
 
60
55
  # transform the data if needed
61
56
  query.each do |h|
62
57
  return h if h[:id] == input[:id]
63
58
  end
64
59
 
65
- {}
60
+ raise Exception.new 'no object found'
66
61
  end
67
62
 
68
63
  def remove input
69
- # open the file
70
- contents = File.read @filename
71
-
72
64
  data = []
73
65
  # parse the json list
74
- query = JSON.parse contents, :symbolize_names => true
66
+ query = load_data
75
67
 
76
68
  # transform the data if needed
77
69
  query.each do |h|
@@ -80,15 +72,28 @@ class FsPlug
80
72
  end
81
73
  end
82
74
 
83
- # save the data back to FS
84
- json_data = JSON.pretty_generate data
85
- File.open(@filename, 'w') {|f| f.write(json_data) }
75
+ save_data data
86
76
 
87
77
  # return true on success
88
78
  true
89
79
  end
90
80
 
81
+ def find input
82
+ data = []
83
+ query = load_data
84
+
85
+ key = input.keys[0]
86
+
87
+ query.each do |h|
88
+ if input[key] == h[key]
89
+ return h
90
+ end
91
+ end
92
+
93
+ raise Exception.new 'no object found'
94
+ end
91
95
  end
92
96
 
93
97
 
94
98
 
99
+
@@ -1,5 +1,10 @@
1
1
  require 'moped'
2
2
 
3
+ # To get the mongo system to work you need to have a counters collection. The
4
+ # code here will increment the seq field which we use to set the id. If you are
5
+ # having problems, you probably need to create the collection and add entries
6
+ # for each other collection you want to have an id sequence.
7
+
3
8
  class MongoPlug
4
9
  def initialize collection
5
10
  @collection = collection
@@ -0,0 +1,94 @@
1
+ require 'json'
2
+ require 'aws/s3'
3
+
4
+ class S3Plug
5
+ include AWS::S3
6
+
7
+ def initialize input
8
+ @filename = input[:filename]
9
+ @bucket = input[:bucket]
10
+
11
+ # you can test locally with the fake-s3 gem: https://github.com/jubos/fake-s3
12
+ # or just swap out for the filesystem plug locally
13
+ s3_key = ENV['S3_KEY'] || '123'
14
+ s3_secret = ENV['S3_SECRET'] || 'abc'
15
+ s3_server = ENV['S3_SERVER'] || '0.0.0.0'
16
+ s3_port = ENV['S3_PORT'] || '10001'
17
+
18
+ AWS::S3::Base.establish_connection!(:access_key_id => s3_key,
19
+ :secret_access_key => s3_secret,
20
+ :server => s3_server,
21
+ :port => s3_port)
22
+ end
23
+
24
+ def load_data
25
+ contents = S3Object.value @filename, @bucket
26
+ JSON.parse contents, :symbolize_names => true
27
+ end
28
+
29
+ def save_data data
30
+ json_data = JSON.pretty_generate data
31
+ S3Object.store @filename, json_data, @bucket
32
+ end
33
+
34
+ def save input
35
+ data = []
36
+ result = load_data
37
+ new_element = true if input[:id] == -1 # by convention set new element flag if id == -1
38
+
39
+ max_id = -1
40
+ # transform the data if needed
41
+ result.each do |h|
42
+ if input[:id] == h[:id]
43
+ h = input
44
+ end
45
+ max_id = h[:id] if h[:id] > max_id
46
+ data << h
47
+ end
48
+
49
+ # add data to the list if it's a new element
50
+ if new_element
51
+ input[:id] = max_id + 1
52
+ data << input
53
+ end
54
+
55
+ save_data data
56
+
57
+ # return the transformed data
58
+ input
59
+ end
60
+
61
+ def list
62
+ load_data
63
+ end
64
+
65
+ def get input
66
+ data = []
67
+ result = load_data
68
+
69
+ # transform the data if needed
70
+ result.each do |h|
71
+ return h if h[:id] == input[:id]
72
+ end
73
+
74
+ {}
75
+ end
76
+
77
+ def remove input
78
+ data = []
79
+ result = load_data
80
+
81
+ # transform the data if needed
82
+ result.each do |h|
83
+ unless h[:id] == input[:id]
84
+ data << h
85
+ end
86
+ end
87
+
88
+ save_data data
89
+
90
+ # return true on success
91
+ true
92
+ end
93
+ end
94
+
@@ -0,0 +1,38 @@
1
+ module Obvious
2
+ module Obj
3
+ class << self
4
+ def included(base)
5
+ base.extend ClassMethods
6
+ end
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ def define method, input = {}, &block
12
+ define_method(method) do |method_input = {}|
13
+ block_input = {}
14
+ method_input.each do |k,v|
15
+ if input[k].nil?
16
+ raise ArgumentError.new "invalid input field #{k}"
17
+ end
18
+
19
+ unless v.is_a? input[k][1]
20
+ raise ArgumentError.new "invalid type for #{k} expected #{input[k][1]}"
21
+ end
22
+
23
+ block_input[input[k][0]] = v
24
+ end
25
+
26
+ input.each do |k,v|
27
+ if block_input[v[0]].nil?
28
+ raise ArgumentError.new "missing input field #{k}"
29
+ end
30
+ end
31
+
32
+ self.instance_exec block_input, &block
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -1,3 +1,3 @@
1
1
  module Obvious
2
- VERSION = "0.0.7"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/obvious.rb CHANGED
@@ -1,3 +1,5 @@
1
1
  require 'obvious/version'
2
2
  require 'obvious/contract'
3
+ require 'obvious/entity'
4
+ require 'obvious/obj'
3
5
  require_relative 'generators/application_generator'
data/obvious.gemspec CHANGED
@@ -9,11 +9,13 @@ Gem::Specification.new do |gem|
9
9
  gem.authors = ["Brian Knapp"]
10
10
  gem.email = ["brianknapp@gmail.com"]
11
11
  gem.description = "A set of tools to build apps using the Obvious Architecture"
12
- gem.summary = "Isn't it Obvious?"
12
+ gem.summary = "Clean Architecture framework"
13
13
  gem.homepage = "http://obvious.retromocha.com/"
14
14
 
15
15
  gem.files = `git ls-files`.split($/)
16
16
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
+
20
+ gem.add_development_dependency "rspec"
19
21
  end
Binary file
@@ -0,0 +1,63 @@
1
+ require_relative 'spec_helper'
2
+ require_relative '../lib/obvious/contract'
3
+
4
+
5
+ describe Hash do
6
+ describe '#has_shape?' do
7
+ it 'should return true for a valid shape' do
8
+ expect({ id: 1 }.has_shape?(id: Integer)).to be true
9
+ end
10
+
11
+ it 'should return false for an invalid shape' do
12
+ expect({ id: 1 }.has_shape?(id: String)).to be false
13
+ end
14
+
15
+ it 'should retrn the invalid field if return_field flag is set' do
16
+ expect({ id: 1 }.has_shape?({id: String}, true)).to eq [false, :id]
17
+ end
18
+
19
+ it 'should allow for nil values to be returned' do
20
+ expect({ id: nil }.has_shape?({id: String})).to be true
21
+ end
22
+ end
23
+ end
24
+
25
+ class TestContract < Obvious::Contract
26
+ contract_for :test, {
27
+ input: { id: Integer },
28
+ output: { id: Integer, value: String }
29
+ }
30
+
31
+ def test input
32
+ { id: 1, value: 'this is a test' }
33
+ end
34
+ end
35
+
36
+ describe Obvious::Contract do
37
+
38
+ describe "#call_method" do
39
+ it 'should return the correct output for valid input and output shapes' do
40
+ tc = TestContract.new
41
+ result = tc.test id: 1
42
+ expect(result).to eq id: 1, value: 'this is a test'
43
+ end
44
+
45
+ it 'should raise a contract input error with bad input' do
46
+ tc = TestContract.new
47
+ expect { tc.test Hash.new }.to raise_error ContractInputError
48
+ end
49
+
50
+ it 'should raise a DataNotFound error if {} is returned' do
51
+ tc = TestContract.new
52
+ allow(tc).to receive(:test_alias).and_return({})
53
+ expect { tc.test id: 1 }.to raise_error DataNotFoundError
54
+ end
55
+
56
+ it 'should raise a contract output error if nil is returned' do
57
+ tc = TestContract.new
58
+ allow(tc).to receive(:test_alias).and_return(nil)
59
+ expect { tc.test id: 1 }.to raise_error ContractOutputError
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,75 @@
1
+ require_relative '../lib/obvious/entity'
2
+
3
+ class Thing < Obvious::Entity
4
+ value :id, Integer
5
+ value :name, String
6
+ end
7
+
8
+ class Thing2 < Obvious::Entity
9
+ value :foo , String
10
+
11
+ validation :something, Proc.new {
12
+ if foo != "hello world"
13
+ msg = "Validation Error: Invalid value for foo, should be 'hello world'"
14
+ raise Obvious::ValidationError.new msg
15
+ end
16
+ }
17
+
18
+ def modify_foo
19
+ @values[:foo] = 100
20
+ end
21
+
22
+ end
23
+
24
+ class Thing3 < Obvious::Entity
25
+ value :foo , String
26
+
27
+ validation :something, Proc.new {
28
+ @values[:foo] = 12
29
+ }
30
+
31
+ end
32
+
33
+ # To test the entity, we are going to use classes that inherit from it instead
34
+ # of poking at it directly. In this case, I think that makes the most sense.
35
+ describe Thing do
36
+ it 'should create a valid object with valid input' do
37
+ input = { name: 'Thing', id: 1 }
38
+ t = Thing.new input
39
+ expect(t.name).to eq 'Thing'
40
+ expect(t.id).to eq 1
41
+ end
42
+
43
+ it 'should raise an error with invalid input types' do
44
+ input = { name: nil, id: nil }
45
+ expect { Thing.new input }.to raise_error Obvious::TypeError
46
+ end
47
+
48
+ it 'should raise an error with extra fields in input' do
49
+ input = { name: 'Thing', id: 1, extra: 'should explode' }
50
+ expect { Thing.new input }.to raise_error Obvious::ShapeError
51
+ end
52
+
53
+ it 'should raise an error when a method tries to modify a value' do
54
+ t = Thing2.new foo: 'hello world'
55
+ expect { t.modify_foo }.to raise_error RuntimeError
56
+ end
57
+
58
+ describe '#to_hash' do
59
+ it 'should return a hash representation of the object' do
60
+ input = { name: 'Thing', id: 1 }
61
+ t = Thing.new input
62
+ expect(t.to_hash).to eq input
63
+ end
64
+ end
65
+
66
+ describe 'validation' do
67
+ it 'should raise a validation error on a failed validation' do
68
+ expect { Thing2.new foo: 'not valid I promise!' }.to raise_error Obvious::ValidationError
69
+ end
70
+
71
+ it 'should raise an error when trying to modify an object in a validation' do
72
+ expect { Thing3.new foo: 'hello world' }.to raise_error RuntimeError
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,34 @@
1
+ require_relative '../../lib/generators/descriptor'
2
+
3
+ require File.expand_path('spec/spec_helper')
4
+
5
+ module Obvious
6
+ module Generators
7
+ describe Descriptor do
8
+ subject {Descriptor.new(yaml_file)}
9
+
10
+ describe "#to_file" do
11
+
12
+ context "when the descriptor is empty" do
13
+ let( :yaml_file ) { {} }
14
+
15
+ it "should raise a meaningful error" do
16
+ expect {subject.to_file}.to raise_error(InvalidDescriptorError)
17
+ end
18
+ end
19
+
20
+ ["Action", "Code", "Description"].each do |section|
21
+ context "when the '#{section}' section is omitted" do
22
+ let( :yaml_file ) {
23
+ {"Action" => "Jackson", "Description" => "This is something"}.delete(section)
24
+ }
25
+
26
+ it "should raise a meaningful error" do
27
+ expect {subject.to_file}.to raise_error(InvalidDescriptorError)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
data/spec/obj_spec.rb ADDED
@@ -0,0 +1,64 @@
1
+ require_relative '../lib/obvious/obj'
2
+
3
+ class TestObj
4
+ include Obvious::Obj
5
+
6
+ def initialize
7
+ @local = 'set!'
8
+ end
9
+
10
+ define :defined_method, with_foo: [:foo, String], also_bar: [:bar, Integer] do |input|
11
+ input
12
+ end
13
+
14
+ define :defined_local do |input|
15
+ @local
16
+ end
17
+
18
+ end
19
+
20
+ describe Obvious::Obj do
21
+
22
+ before do
23
+ @test = TestObj.new
24
+ end
25
+
26
+ describe 'self.define' do
27
+ it 'should do the right thing with correct input' do
28
+ result = @test.defined_method with_foo: 'hello', also_bar: 12
29
+ expect(result).to eq foo: 'hello', bar: 12
30
+ end
31
+
32
+ it 'should have access to instance variables' do
33
+ result = @test.defined_local
34
+ expect(result).to eq 'set!'
35
+ end
36
+
37
+ it 'should raise an error for missing parameters' do
38
+ expect { @test.defined_method with_foo: 'hello' }.to raise_error { |error|
39
+ expect(error).to be_a ArgumentError
40
+ expect(error.message).to eq 'missing input field also_bar'
41
+ }
42
+ end
43
+
44
+ it 'should raise an error for extra parameters' do
45
+ expect { @test.defined_method with_foo: 'hello', also_bar: 12, and_extra: 'this is extra!' }.to raise_error { |error|
46
+ expect(error).to be_a ArgumentError
47
+ expect(error.message).to eq 'invalid input field and_extra'
48
+ }
49
+ end
50
+
51
+ it 'should raise an error for invalid types' do
52
+ expect { @test.defined_method with_foo: 1, also_bar: 12 }.to raise_error { |error|
53
+ expect(error).to be_a ArgumentError
54
+ expect(error.message).to eq 'invalid type for with_foo expected String'
55
+ }
56
+
57
+ expect {@test.defined_method with_foo: 'hello', also_bar: nil }.to raise_error { |error|
58
+ expect(error).to be_a ArgumentError
59
+ expect(error.message).to eq 'invalid type for also_bar expected Integer'
60
+ }
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,3 @@
1
+ RSpec.configure do |c|
2
+ c.mock_with :rspec
3
+ end
metadata CHANGED
@@ -1,16 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: obvious
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
5
- prerelease:
4
+ version: 0.1.0
6
5
  platform: ruby
7
6
  authors:
8
7
  - Brian Knapp
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-01-19 00:00:00.000000000 Z
13
- dependencies: []
11
+ date: 2022-01-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
14
27
  description: A set of tools to build apps using the Obvious Architecture
15
28
  email:
16
29
  - brianknapp@gmail.com
@@ -19,8 +32,10 @@ executables:
19
32
  extensions: []
20
33
  extra_rdoc_files: []
21
34
  files:
22
- - .gitignore
35
+ - ".github/workflows/obvious.yml"
36
+ - ".gitignore"
23
37
  - Gemfile
38
+ - Gemfile.lock
24
39
  - LICENSE.txt
25
40
  - README.md
26
41
  - Rakefile
@@ -30,35 +45,47 @@ files:
30
45
  - lib/generators/helpers/application.rb
31
46
  - lib/obvious.rb
32
47
  - lib/obvious/contract.rb
48
+ - lib/obvious/entity.rb
33
49
  - lib/obvious/files/Rakefile
34
50
  - lib/obvious/files/external/fs_plug.rb
35
51
  - lib/obvious/files/external/mongo_plug.rb
36
52
  - lib/obvious/files/external/mysql_plug.rb
53
+ - lib/obvious/files/external/s3_plug.rb
54
+ - lib/obvious/obj.rb
37
55
  - lib/obvious/version.rb
38
56
  - obvious.gemspec
57
+ - spec/.spec_helper.rb.swp
58
+ - spec/contract_spec.rb
59
+ - spec/entity_spec.rb
60
+ - spec/generators/descriptor_spec.rb
61
+ - spec/obj_spec.rb
62
+ - spec/spec_helper.rb
39
63
  homepage: http://obvious.retromocha.com/
40
64
  licenses: []
65
+ metadata: {}
41
66
  post_install_message:
42
67
  rdoc_options: []
43
68
  require_paths:
44
69
  - lib
45
70
  required_ruby_version: !ruby/object:Gem::Requirement
46
- none: false
47
71
  requirements:
48
- - - ! '>='
72
+ - - ">="
49
73
  - !ruby/object:Gem::Version
50
74
  version: '0'
51
75
  required_rubygems_version: !ruby/object:Gem::Requirement
52
- none: false
53
76
  requirements:
54
- - - ! '>='
77
+ - - ">="
55
78
  - !ruby/object:Gem::Version
56
79
  version: '0'
57
80
  requirements: []
58
- rubyforge_project:
59
- rubygems_version: 1.8.17
81
+ rubygems_version: 3.0.3
60
82
  signing_key:
61
- specification_version: 3
62
- summary: Isn't it Obvious?
63
- test_files: []
64
- has_rdoc:
83
+ specification_version: 4
84
+ summary: Clean Architecture framework
85
+ test_files:
86
+ - spec/.spec_helper.rb.swp
87
+ - spec/contract_spec.rb
88
+ - spec/entity_spec.rb
89
+ - spec/generators/descriptor_spec.rb
90
+ - spec/obj_spec.rb
91
+ - spec/spec_helper.rb