obvious 0.0.7 → 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.
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