seat-belt 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +29 -0
- data/.travis.yml +6 -0
- data/Changelog.md +55 -0
- data/Gemfile +15 -0
- data/Guardfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +705 -0
- data/Rakefile +1 -0
- data/ext/.gitkeep +0 -0
- data/lib/seatbelt.rb +37 -0
- data/lib/seatbelt/collections/collection.rb +56 -0
- data/lib/seatbelt/core.rb +15 -0
- data/lib/seatbelt/core/callee.rb +35 -0
- data/lib/seatbelt/core/eigenmethod.rb +150 -0
- data/lib/seatbelt/core/eigenmethod_proxy.rb +45 -0
- data/lib/seatbelt/core/ext/core_ext.rb +0 -0
- data/lib/seatbelt/core/gate.rb +198 -0
- data/lib/seatbelt/core/ghost_tunnel.rb +81 -0
- data/lib/seatbelt/core/implementation.rb +158 -0
- data/lib/seatbelt/core/interface.rb +51 -0
- data/lib/seatbelt/core/iterators/method_config.rb +50 -0
- data/lib/seatbelt/core/lookup_table.rb +101 -0
- data/lib/seatbelt/core/pool.rb +90 -0
- data/lib/seatbelt/core/property.rb +161 -0
- data/lib/seatbelt/core/proxy.rb +135 -0
- data/lib/seatbelt/core/synthesizeable.rb +50 -0
- data/lib/seatbelt/core/terminal.rb +59 -0
- data/lib/seatbelt/dependencies.rb +5 -0
- data/lib/seatbelt/document.rb +175 -0
- data/lib/seatbelt/errors.rb +1 -0
- data/lib/seatbelt/errors/errors.rb +150 -0
- data/lib/seatbelt/gate_config.rb +59 -0
- data/lib/seatbelt/ghost.rb +140 -0
- data/lib/seatbelt/models.rb +9 -0
- data/lib/seatbelt/seatbelt.rb +10 -0
- data/lib/seatbelt/synthesizer.rb +3 -0
- data/lib/seatbelt/synthesizers/document.rb +16 -0
- data/lib/seatbelt/synthesizers/mongoid.rb +16 -0
- data/lib/seatbelt/synthesizers/synthesizer.rb +146 -0
- data/lib/seatbelt/tape.rb +2 -0
- data/lib/seatbelt/tape_deck.rb +71 -0
- data/lib/seatbelt/tapes/tape.rb +105 -0
- data/lib/seatbelt/tapes/util/delegate.rb +56 -0
- data/lib/seatbelt/translator.rb +66 -0
- data/lib/seatbelt/version.rb +3 -0
- data/seatbelt.gemspec +27 -0
- data/spec/lib/seatbelt/core/eigenmethod_spec.rb +102 -0
- data/spec/lib/seatbelt/core/gate_spec.rb +521 -0
- data/spec/lib/seatbelt/core/ghost_tunnel_spec.rb +21 -0
- data/spec/lib/seatbelt/core/lookup_table_spec.rb +234 -0
- data/spec/lib/seatbelt/core/pool_spec.rb +270 -0
- data/spec/lib/seatbelt/core/proxy_spec.rb +108 -0
- data/spec/lib/seatbelt/core/terminal_spec.rb +184 -0
- data/spec/lib/seatbelt/document_spec.rb +287 -0
- data/spec/lib/seatbelt/gate_config_spec.rb +98 -0
- data/spec/lib/seatbelt/ghost_spec.rb +568 -0
- data/spec/lib/seatbelt/synthesizers/document_spec.rb +47 -0
- data/spec/lib/seatbelt/synthesizers/mongoid_spec.rb +134 -0
- data/spec/lib/seatbelt/synthesizers/synthesizer_spec.rb +112 -0
- data/spec/lib/seatbelt/tape_deck_spec.rb +180 -0
- data/spec/lib/seatbelt/tapes/tape_spec.rb +115 -0
- data/spec/lib/seatbelt/translator_spec.rb +108 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/implementations/seatbelt_environment.rb +19 -0
- data/spec/support/shared_examples/shared_api_class.rb +7 -0
- data/spec/support/shared_examples/shared_collection_child.rb +7 -0
- data/spec/support/worlds/eigenmethod_world.rb +7 -0
- metadata +205 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 54679b016a9bdcf5c7c866fbeee5c5a0994f5205
|
4
|
+
data.tar.gz: adec3307b740eeb81d088ca8dddc9640164bb0f3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a514352e359790c24860cc10d9de19179da3305188ce928c1965e2f764eaee19493691ecfbb8d97ef9790e117bd22a9ecdd0551e56b74ec5ac4254460bef0adb
|
7
|
+
data.tar.gz: 29e9cc0326deca84d5d34d7f5e3649d7d3f9ba117434d2864b5ebe51665f03b37c13dec3e909bef7aa45db2aed3e63f7e6f37d7144f200ffb793d6c45c103c0c
|
data/.gitignore
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
rspec_results.html
|
8
|
+
InstalledFiles
|
9
|
+
_yardoc
|
10
|
+
coverage
|
11
|
+
doc/
|
12
|
+
lib/bundler/man
|
13
|
+
pkg
|
14
|
+
rdoc
|
15
|
+
spec/reports
|
16
|
+
test/tmp
|
17
|
+
test/version_tmp
|
18
|
+
tmp
|
19
|
+
.tags
|
20
|
+
|
21
|
+
# RVM
|
22
|
+
.ruby-version
|
23
|
+
.ruby-gemset
|
24
|
+
.rvmrc
|
25
|
+
|
26
|
+
# OSX
|
27
|
+
.DS_Store
|
28
|
+
|
29
|
+
*.tags*
|
data/.travis.yml
ADDED
data/Changelog.md
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
## v0.10.0 (2014-01-17)
|
2
|
+
|
3
|
+
* Add configuration for Translator the specify custom values
|
4
|
+
* Rename TravelAgent to Translator
|
5
|
+
* Remove Seatbelt::Collections::Collection subclasses
|
6
|
+
* Remove Seatbelt::Models
|
7
|
+
* Support mass assignment of properties and (if present) attributes
|
8
|
+
|
9
|
+
## v0.4.2 (2013-12-17)
|
10
|
+
|
11
|
+
* Include superclass method implementations to match directive
|
12
|
+
* Add property defining and matching
|
13
|
+
|
14
|
+
## v0.4.1 (2013-11-19)
|
15
|
+
|
16
|
+
* Change internal API to Virtus 1.0.0
|
17
|
+
* Fixes an issue that prevents class methods used as implementation methods
|
18
|
+
* Add #interface and #implementation directives to have a cleaner API
|
19
|
+
* Add support for specifying arguments to a API method definition.
|
20
|
+
* Add tunneling from API class instances to implementation class instances
|
21
|
+
* Add tunneling of private API properties
|
22
|
+
* Add synthesize support on instance level between two objects.
|
23
|
+
|
24
|
+
## v0.4.0 (2013-30-09)
|
25
|
+
|
26
|
+
* New internal API method delegating by introducing an Eigenmethod object
|
27
|
+
|
28
|
+
## v0.3.3 (2013-09-25)
|
29
|
+
|
30
|
+
* Fixes an error that raises TypeMissmatchError if multiple has_many is used
|
31
|
+
* Fixes assigning identical proxy object to multiple implementation classes.
|
32
|
+
|
33
|
+
## v0.3.2 (2013-09-24)
|
34
|
+
|
35
|
+
* API class methods are now delegatable to implementation class methods
|
36
|
+
|
37
|
+
## v0.3.1 (2013-09-17)
|
38
|
+
|
39
|
+
* Add tapes support for TQL
|
40
|
+
|
41
|
+
## v0.3.0 (2013-09-06)
|
42
|
+
|
43
|
+
* Add association support to Seatbelt::Document (has_many, has)
|
44
|
+
|
45
|
+
## v0.2.0 (2013-08-27)
|
46
|
+
|
47
|
+
* Add Dynamic Proxy pattern to have direct access to proxy objects klass methods
|
48
|
+
* Add validations of attributes to Seatbelt::Document
|
49
|
+
|
50
|
+
## v0.1.0 (2013-08-21)
|
51
|
+
|
52
|
+
* Add API method definition
|
53
|
+
* Add forward declaration of API methods to implementation classes
|
54
|
+
* Add proxy object to access API class or instance in implementation methods
|
55
|
+
* Add attributes to Seatbelt::Document classes
|
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in seatbelt.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :test do
|
7
|
+
gem "rspec"
|
8
|
+
gem "mongoid", "~>3.1.5"
|
9
|
+
end
|
10
|
+
|
11
|
+
group :development do
|
12
|
+
gem "step-up", "~> 0.9.1"
|
13
|
+
gem 'guard-rspec'
|
14
|
+
gem 'ruby_gntp'
|
15
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard :rspec do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
|
9
|
+
# Turnip features and steps
|
10
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
11
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
|
12
|
+
end
|
13
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Daniel Schmidt
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,705 @@
|
|
1
|
+
![Seatbelt](http://datenspiel.s3.amazonaws.com/vagalo/seatbelt_top.jpg)
|
2
|
+
# Seatbelt
|
3
|
+
|
4
|
+
Tool to define implementation interfaces that should be decoupled from their
|
5
|
+
implementations. (A Ruby header file approach.)
|
6
|
+
|
7
|
+
## Development instructions
|
8
|
+
|
9
|
+
### Install RSpec Git Pre-commit hook
|
10
|
+
|
11
|
+
To get a working testsuite before committing code to the repository you have to install a Git pre-commit hook that prevends you from committing unless your specs passed or are pending.
|
12
|
+
|
13
|
+
If you haven't <code>wget</code> installed already, install it using homebrew:
|
14
|
+
|
15
|
+
```
|
16
|
+
brew install wget
|
17
|
+
```
|
18
|
+
|
19
|
+
Install the hook:
|
20
|
+
|
21
|
+
```
|
22
|
+
wget -O .git/hooks/pre-commit https://raw.github.com/markhazlett/RSpec-Pre-commit-Git-Hook/master/rspec-precommit
|
23
|
+
```
|
24
|
+
|
25
|
+
After installing, call
|
26
|
+
|
27
|
+
```
|
28
|
+
chmod +x .git/hooks/pre-commit
|
29
|
+
```
|
30
|
+
|
31
|
+
to make it executable.
|
32
|
+
|
33
|
+
### Testing
|
34
|
+
|
35
|
+
Although running the testsuite with [Guard](http://guardgem.org/) I highly recommened the
|
36
|
+
[RubyTest](https://sublime.wbond.net/packages/RubyTest) Sublime Text package.
|
37
|
+
|
38
|
+
|
39
|
+
## Installation
|
40
|
+
|
41
|
+
Add this line to your application's Gemfile:
|
42
|
+
|
43
|
+
gem 'seatbelt'
|
44
|
+
|
45
|
+
And then execute:
|
46
|
+
|
47
|
+
$ bundle
|
48
|
+
|
49
|
+
Or install it yourself as:
|
50
|
+
|
51
|
+
$ gem install seatbelt
|
52
|
+
|
53
|
+
## Usage
|
54
|
+
|
55
|
+
### Defining API classes and API meta-methods
|
56
|
+
|
57
|
+
Defining classes that act like API classes is a very simple step. Define a plain
|
58
|
+
Ruby class and include the
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
Seatbelt::Ghost
|
62
|
+
```
|
63
|
+
|
64
|
+
module.
|
65
|
+
|
66
|
+
That gives you access to the ```interface``` class method. API meta-methods are not implemented, they will only be defined. Be sure that you have a specification of these methods like passing how many arguments and if a block is required.
|
67
|
+
|
68
|
+
```interface``` takes an argument that defines which object level the method should be
|
69
|
+
callable at (The implementation class has to define a matcher for this. See below.)
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
class Hotel
|
73
|
+
include Seatbelt::Gate
|
74
|
+
|
75
|
+
interface :class do
|
76
|
+
define :find_nearby,
|
77
|
+
:args => [:options]
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
```
|
82
|
+
|
83
|
+
```define``` expects at least one argument, it's method name. The second argument is optional but has to be a ```Hash```.
|
84
|
+
|
85
|
+
If your API meta-method expects arguments you have to specify the list of arguments.
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
define :find_nearby,
|
89
|
+
:block_required => false,
|
90
|
+
:args => [:options]
|
91
|
+
```
|
92
|
+
|
93
|
+
With this, a class method ```find_nearby``` is defined at the ```Hotel``` class. Calling that method is straight forward:
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
Hotel.find_nearby(:city => "London") # returns all Hotels near London
|
97
|
+
```
|
98
|
+
|
99
|
+
To define an instance API meta-method at the ```Hotel``` class, just pass ```:instance``` to ```:interface``` method:
|
100
|
+
|
101
|
+
```ruby
|
102
|
+
interface :instance do
|
103
|
+
define :number_of_rooms_with_tv_sets
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
And, like above, calling is just pure Ruby:
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
hotel = Hotel.new
|
111
|
+
hotel.number_of_rooms_with_tv_sets
|
112
|
+
```
|
113
|
+
|
114
|
+
If your API meta-method specification says that the method requires a block, just pass truthy
|
115
|
+
to the ```:block_required``` key.
|
116
|
+
|
117
|
+
A note about method argument specification:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
define :foo,
|
121
|
+
:args => ["name", "*args", "&block"]
|
122
|
+
```
|
123
|
+
|
124
|
+
expects that you have an implementation body - let's say - like this:
|
125
|
+
|
126
|
+
```
|
127
|
+
def implement_foo(name, *args, &block)
|
128
|
+
# ...
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
**Defining API properties**
|
133
|
+
|
134
|
+
If the API class should provide properties use the ```define_property``` for single property or ```define_properties``` for multiple property definiton.
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
interface :instance do
|
138
|
+
define_property :foo
|
139
|
+
define_properties :bar, :foobar
|
140
|
+
end
|
141
|
+
```
|
142
|
+
|
143
|
+
If you want to update a bunch of properties at once, you must mark them as accessible.
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
interface :instance do
|
147
|
+
define_property :foo
|
148
|
+
define_properties :bar, :foobar
|
149
|
+
|
150
|
+
property_accessible :foo, :bar
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
foo = Foo.new
|
155
|
+
|
156
|
+
foo.properties = { :foo => 12, :bar => "glasses", :foobar => "of beer." }
|
157
|
+
```
|
158
|
+
|
159
|
+
In the example above, the ```foobar``` property will not be set, because it's not marked as accessible.
|
160
|
+
|
161
|
+
### Implementing API meta-methods
|
162
|
+
|
163
|
+
Defining API meta-methods is simple, but how do you implement the logic of these methods? Use a plain Ruby class and include the ```Seatbelt::Gate``` module.
|
164
|
+
|
165
|
+
Then it's possible to associate the implemention method with the API meta-method definition.
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
class ImplementHotel
|
169
|
+
include Seatbelt::Gate
|
170
|
+
|
171
|
+
implementation "Hotel", :class do
|
172
|
+
match 'hotels_nearby_city' => 'find_nearby'
|
173
|
+
match 'all' => 'all'
|
174
|
+
end
|
175
|
+
|
176
|
+
def hotels_nearby_city(options={})
|
177
|
+
# ...
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.all
|
181
|
+
# ....
|
182
|
+
end
|
183
|
+
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
The ```implementation``` directive takes two arguments. The first one is the method name of the API class, the second one is the object level the implementation is defined on (```:instance```, ```:class```).
|
188
|
+
|
189
|
+
The ```match``` directive takes an Hash containing the implementation method name as key and the API method as value.
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
class ImplementHotel
|
193
|
+
include Seatbelt::Gate
|
194
|
+
|
195
|
+
# Class methods.
|
196
|
+
implementation "Hotel", :class do
|
197
|
+
match 'hotels_nearby_city' => 'find_nearby'
|
198
|
+
end
|
199
|
+
|
200
|
+
# Instance methods.
|
201
|
+
implementation "Hotel", :instance do
|
202
|
+
match 'rooms_with_tv' => 'number_of_rooms_with_tv_sets'
|
203
|
+
end
|
204
|
+
|
205
|
+
def hotels_nearby_city(options={})
|
206
|
+
# ...
|
207
|
+
end
|
208
|
+
|
209
|
+
def rooms_with_tv
|
210
|
+
#....
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
With the ```match``` directive it is also possible to bind an implementation method from the superclass to the given interface class:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
|
220
|
+
class MetaBook
|
221
|
+
# An implementation method used for all childs of MetaBook
|
222
|
+
def general_stuff_for_books
|
223
|
+
#...
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
class ImplementationNovel < MetaBook
|
228
|
+
include Seatbelt::Gate
|
229
|
+
|
230
|
+
implementation "Novel", :instance do
|
231
|
+
match 'general_stuff_for_books' => 'publisher', :superclass => true
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
235
|
+
|
236
|
+
```
|
237
|
+
|
238
|
+
Note that this is only possible for ```:instance``` level implementations.
|
239
|
+
|
240
|
+
(Thanks to @LarsM for suggesting this feature.)
|
241
|
+
|
242
|
+
**Using delegated methods**
|
243
|
+
|
244
|
+
If you want to use a method as implementation method that is delegated to another object you can mark these methods as
|
245
|
+
delegated and Seatbelt will recognize them too.
|
246
|
+
|
247
|
+
```ruby
|
248
|
+
class BookImplementation
|
249
|
+
delegate :distributor_name, to: :@distributor
|
250
|
+
|
251
|
+
implementation "Book", :instance do
|
252
|
+
match 'distributor_name' => 'distributor', delegated: true
|
253
|
+
end
|
254
|
+
end
|
255
|
+
```
|
256
|
+
|
257
|
+
**Implementing API properties**
|
258
|
+
|
259
|
+
Similiar to ```match``` associate the properties of your implementation class and your API class with ```match_property```.
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
# Having an identical property wthin the interface.
|
263
|
+
implementation "Book", :instance do
|
264
|
+
match_property 'author'
|
265
|
+
end
|
266
|
+
|
267
|
+
# Property names differ
|
268
|
+
implementation "Book", :instance do
|
269
|
+
match_property 'implementation_title' => 'title'
|
270
|
+
end
|
271
|
+
|
272
|
+
# Property is defined in the superclass
|
273
|
+
implementation "Novel", :instance do
|
274
|
+
match_property :publisher, :superclass => true
|
275
|
+
end
|
276
|
+
```
|
277
|
+
|
278
|
+
|
279
|
+
### Accessing the API class in implementations of API meta-methods
|
280
|
+
|
281
|
+
You can access the API class within the implementation methods through the
|
282
|
+
```proxy``` object.
|
283
|
+
|
284
|
+
Depending the type of method you are implementing, ```proxy``` will be a different scope:
|
285
|
+
|
286
|
+
* implementing a later class method, ```proxy``` is the class of the API class defined in ```implementation```
|
287
|
+
* implementing a later instance method, ```proxy``` is the instance of the API class defined in ```implementation```
|
288
|
+
|
289
|
+
The ```proxy``` object provides a ```call``` method to access the ```proxy``` methods. It expects the method name to call, an argument list and an optional block.
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
def rooms_with_tv
|
293
|
+
excluded_rooms = proxy.call(:second_floor)
|
294
|
+
room_criteria = proxy.call(:criteria, :not => :gallery)
|
295
|
+
# ....
|
296
|
+
end
|
297
|
+
```
|
298
|
+
|
299
|
+
```#call``` could be omitted and message send directly to the API class or instance receiver.
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
def rooms_with_tv
|
303
|
+
excluded_rooms = proxy.second_floor
|
304
|
+
room_criteria = proxy.criteria(:not => :gallery)
|
305
|
+
end
|
306
|
+
```
|
307
|
+
|
308
|
+
**Hint**: Chaining API classes is possible by returning ```proxy.object``` from the implementation method.
|
309
|
+
|
310
|
+
**Note:** If you want to delegate a class method of your implementation class to a class API method by using the ```:type``` key, the proxy object acts as the class object of the API class.
|
311
|
+
|
312
|
+
### Defining attributes in API classes
|
313
|
+
|
314
|
+
You can define attributes within an API class by including the ```Seatbelt::Document```module.
|
315
|
+
|
316
|
+
```ruby
|
317
|
+
class Airport
|
318
|
+
include Seatbelt::Ghost
|
319
|
+
include Seatbelt::Document
|
320
|
+
|
321
|
+
attribute :name, String
|
322
|
+
attribute :lat, Float
|
323
|
+
attribute :lng, Float
|
324
|
+
|
325
|
+
interface :instance do
|
326
|
+
define :identifier
|
327
|
+
end
|
328
|
+
|
329
|
+
end
|
330
|
+
```
|
331
|
+
|
332
|
+
To access the attributes within an implementation class use the ```proxy``` and it's ```call``` method.
|
333
|
+
|
334
|
+
For more informations about attributes see the [Virtus](https://github.com/solnic/virtus) project.
|
335
|
+
|
336
|
+
**A note about attributes and Mongoid**: If you include ```mongoid``` in your project and any ```attribute``` should be a boolean, you have use the full path to the type:
|
337
|
+
|
338
|
+
```ruby
|
339
|
+
attribute :is_read, Virtus::Attribute::Boolean
|
340
|
+
```
|
341
|
+
|
342
|
+
For further informations see: https://github.com/solnic/virtus/issues/132#issuecomment-11611142
|
343
|
+
|
344
|
+
|
345
|
+
### Defining associations between objects
|
346
|
+
|
347
|
+
Associations between objects is possible in two ways if ```Seatbelt::Document``` is included in the class.
|
348
|
+
|
349
|
+
**one-to-many**
|
350
|
+
|
351
|
+
```ruby
|
352
|
+
module Seatbelt
|
353
|
+
module Models
|
354
|
+
class Airport
|
355
|
+
include Seatbelt::Document
|
356
|
+
|
357
|
+
has_many :flights, Seatbelt::Models::Flight
|
358
|
+
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
```
|
363
|
+
|
364
|
+
A ```has_many``` relationship definition takes two arguments:
|
365
|
+
|
366
|
+
* the association name
|
367
|
+
* the class used for a single item in the relationship collection
|
368
|
+
|
369
|
+
That creates an accessor method acts like an ```Array```.
|
370
|
+
|
371
|
+
You can add items to the collection (association) in two ways:
|
372
|
+
|
373
|
+
*instance level*
|
374
|
+
|
375
|
+
```ruby
|
376
|
+
airport = Seatbelt::Models::Airport.where(:name => "London Stansted")
|
377
|
+
Flights.find_to(airport.name).each do |flight|
|
378
|
+
aiport.flights << flight
|
379
|
+
end
|
380
|
+
```
|
381
|
+
|
382
|
+
*attribute level*
|
383
|
+
|
384
|
+
```ruby
|
385
|
+
airport = Seatbelt::Models::Airport.where(:name => "London Stansted")
|
386
|
+
GetFromTheInternet.fetch_flights.each do |flight|
|
387
|
+
aiport.flights << {:number => flight["fn_num"], :return_flight_at => flight["RRUECK"]}
|
388
|
+
end
|
389
|
+
```
|
390
|
+
|
391
|
+
**one-to-one**
|
392
|
+
|
393
|
+
```ruby
|
394
|
+
module Seatbelt
|
395
|
+
module Models
|
396
|
+
class Hotel
|
397
|
+
include Seatbelt::Document
|
398
|
+
|
399
|
+
has :region
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
```
|
404
|
+
|
405
|
+
A ```has``` association takes two arguments where the last one is optional.
|
406
|
+
|
407
|
+
* the association name
|
408
|
+
* the class used for the assocation
|
409
|
+
|
410
|
+
If the second argument is omitted ```has``` guessed the corrosping model class. In the example above it will use the
|
411
|
+
```Seatbelt::Models::Region``` class.
|
412
|
+
|
413
|
+
You can assign an object to the association the same way as assigning an attribute.
|
414
|
+
|
415
|
+
### Synthesize objects
|
416
|
+
|
417
|
+
Synthesizing objects is only available at the instance level. By now Seatbelt will only synthesize the state of an object, not its behaviour!
|
418
|
+
|
419
|
+
To synthesize an implementation class instance and the proxy object, add ```synthesize``` to the Implementation class.
|
420
|
+
|
421
|
+
```ruby
|
422
|
+
class ImplementationAirport
|
423
|
+
include Seatbelt::Gate
|
424
|
+
include Mongoid::Document
|
425
|
+
|
426
|
+
field :name, :type => String
|
427
|
+
field :lat, :type => Float
|
428
|
+
field :lng, :type => Float
|
429
|
+
|
430
|
+
synthesize :from => "Seatbelt::Models::Airport",
|
431
|
+
:adapter => "Seatbelt::Synthesizers::Mongoid"
|
432
|
+
end
|
433
|
+
```
|
434
|
+
|
435
|
+
Then - every time ```proxy.[attribute_name]``` is changed within the implementation class - the instance of the implementation class is changed too. And vice versa:
|
436
|
+
|
437
|
+
```ruby
|
438
|
+
aiport = Seatbelt::Models::Airport.new(:name => "London Stansted")
|
439
|
+
# in implementation class self.name will be "London Stansted"
|
440
|
+
|
441
|
+
# in a implementation method
|
442
|
+
def something
|
443
|
+
proxy.name = "London Gatewick"
|
444
|
+
p self.name # => "London Gatewick"
|
445
|
+
end
|
446
|
+
```
|
447
|
+
|
448
|
+
If attribute names are the same on both sides, all is fine. If not, the implementation class has to implement a ```synthesize_map``` method where the keys are the attributes from the ```API class``` and the values are attributes from the ```implementation class```.
|
449
|
+
|
450
|
+
```ruby
|
451
|
+
class ImplementAirport
|
452
|
+
include Seatbelt::Gate
|
453
|
+
include Mongoid::Document
|
454
|
+
|
455
|
+
field :l_name, :type => String
|
456
|
+
field :gidd_lat, :type => Float
|
457
|
+
field :gidd_lng, :type => Float
|
458
|
+
|
459
|
+
synthesize :from => "Seatbelt::Models::Airport",
|
460
|
+
:adapter => "Seatbelt::Synthesizers::Mongoid"
|
461
|
+
|
462
|
+
synthesize_map :name => :l_name, :lat => :gidd_lat, :lng => :gidd_lng
|
463
|
+
end
|
464
|
+
```
|
465
|
+
|
466
|
+
### Defining custom synthesizers
|
467
|
+
|
468
|
+
Seatbelt provides to synthesizers:
|
469
|
+
|
470
|
+
* ```Seatbelt::Synthesizers::Document```
|
471
|
+
* ```Seatbelt::Synthesizers::Mongoid```
|
472
|
+
|
473
|
+
The first one synthesizes ```Seatbelt::Document``` or ```Virtus``` based implementation classes. The second one synthesizes ```Mongoid::Document``` based implementation classed.
|
474
|
+
|
475
|
+
Defining custom synthesizers helpful, if
|
476
|
+
|
477
|
+
* the implementation class uses a not supported backend
|
478
|
+
* only a few attributes should be synthesized that exists on both sides
|
479
|
+
|
480
|
+
A Synthesizer is a plain Ruby class which includes the ```Seatbelt::Synthesizer``` module and implements a ```synthesizable_attributes``` method.
|
481
|
+
|
482
|
+
```ruby
|
483
|
+
class CustomSynthesizer
|
484
|
+
include Seatbelt::Synthesizer
|
485
|
+
|
486
|
+
def synthesizable_attributes
|
487
|
+
[:l_name, :gidd_lat]
|
488
|
+
end
|
489
|
+
end
|
490
|
+
```
|
491
|
+
|
492
|
+
Which only synthesizes the ```:l_name``` and ```:gidd_lat``` properties.
|
493
|
+
|
494
|
+
### Tunneling from API class instances to implementation class instances
|
495
|
+
|
496
|
+
Any API class that implements Seatbelt::Ghost can have access to its
|
497
|
+
implementation class instance. This behaviour has to be enabled before using
|
498
|
+
because its a violation of the Public/Private API approach.
|
499
|
+
|
500
|
+
(**And yes - in Ruby private methods are not really private methods.**)
|
501
|
+
|
502
|
+
Accessing the implementation instance is only available after the API Class
|
503
|
+
was instantiated.
|
504
|
+
|
505
|
+
Example:
|
506
|
+
|
507
|
+
```ruby
|
508
|
+
class Hotel
|
509
|
+
include Seatbelt::Ghost
|
510
|
+
|
511
|
+
enable_tunneling! # access to the implementation instance is now
|
512
|
+
# possible.
|
513
|
+
|
514
|
+
end
|
515
|
+
|
516
|
+
class ImplementationHotel
|
517
|
+
include Seatbelt::Document
|
518
|
+
include Seatbelt::Gate
|
519
|
+
|
520
|
+
attribute :ignore_dirty_rooms, Boolean
|
521
|
+
|
522
|
+
end
|
523
|
+
|
524
|
+
hotel = new Hotel
|
525
|
+
hotel.tunnel(:ignore_dirty_rooms=,false)
|
526
|
+
```
|
527
|
+
|
528
|
+
Passing blocks is also available if the accessed method supports blocks
|
529
|
+
|
530
|
+
```ruby
|
531
|
+
class ImplementationHotel
|
532
|
+
include Seatbelt::Document
|
533
|
+
include Seatbelt::Gate
|
534
|
+
|
535
|
+
attribute :ignore_dirty_rooms, Boolean
|
536
|
+
|
537
|
+
def filter_rooms(sections)
|
538
|
+
rooms = self.rooms.map{|room| sections.include?(room_type)}
|
539
|
+
yield(rooms)
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
hotel.tunnel(:filter_rooms, ["shower, kitchen"]) do |rooms|
|
544
|
+
rooms.select do |room|
|
545
|
+
# do something
|
546
|
+
end
|
547
|
+
end
|
548
|
+
```
|
549
|
+
|
550
|
+
**Note** that this is a dangerous approach and should be avoided. If you change the implementation layer and you are using tunneling from API classes to Implementation classes you have to make sure that the new implementation layer provides the attribute or
|
551
|
+
method you are tunneling to with your API class instance.
|
552
|
+
|
553
|
+
To disable tunneling just call the ```disable_tunneling!``` class method.
|
554
|
+
|
555
|
+
### Man ... we need a translator here
|
556
|
+
|
557
|
+
> "The Babel fish," said The Hitchhiker's Guide to the Galaxy quietly, "is small, yellow and leech-like, and probably the oddest thing in the Universe. It feeds on brainwave energy received not from its own carrier but from those around it. It absorbs all unconscious mental frequencies from this brainwave energy to nourish itself with. It then excretes into the mind of its carrier a telepathic matrix formed by combining the conscious thought frequencies with nerve signals picked up from the speech centres of the brain which has supplied them. The practical upshot of all this is that if you stick a Babel fish in your ear you can instantly understand anything in any form of language. The speech patterns you actually hear decode the brainwave matrix which has been fed into your mind by your Babel fish.
|
558
|
+
|
559
|
+
*(The Hitchhiker's Guide to the Galaxy)*
|
560
|
+
|
561
|
+
Let's talk about basics of the TQL (Translation Query Language). TQL is basically a query formed in a natural sentence (language doesn't matter) after a defined syntax.
|
562
|
+
|
563
|
+
TQL basic support is implemented in Seatbelt v0.4. Basic support means that you are able to implement your own translations and tapes.
|
564
|
+
|
565
|
+
**Tapes?**
|
566
|
+
|
567
|
+
A tape is collection of ```translate``` blocks that will translate a query into some business logic. A tape is added to a tape deck that will play the corrosponding tape section to a given query (or better said - call the translation of this query).
|
568
|
+
|
569
|
+
Let's take a closer look at what a tape deck is and what tapes are.
|
570
|
+
|
571
|
+
Any class could act as a tape deck by including the ```Seatbelt::TapeDeck``` module. Mostly that classes are also including the ```Seatbelt::Document``` module.
|
572
|
+
|
573
|
+
Adding the ```Seatbelt::TapeDeck``` module to a class gives the class the opportunity to
|
574
|
+
|
575
|
+
* add tapes to the class
|
576
|
+
* answer a query with a translation defined within a tape
|
577
|
+
|
578
|
+
Before diving into some example code, let's record a tape. As mentioned above, a tape is just a bunch of translation blocks defining the query syntax and an associated logic implementation.
|
579
|
+
|
580
|
+
To have translation implemented or defined you should be familiar with regular expressions, the core tool of a tape.
|
581
|
+
|
582
|
+
A tape class inherits from ```Seatbelt::Tape``` and is very simple to implement.
|
583
|
+
|
584
|
+
```ruby
|
585
|
+
class PubTape < Seatbelt::Tape
|
586
|
+
|
587
|
+
translate /Gimme (\d+) beers!/ do |sentence, count_of_beer|
|
588
|
+
#
|
589
|
+
end
|
590
|
+
|
591
|
+
end
|
592
|
+
```
|
593
|
+
|
594
|
+
A ```translate```block takes arguments. If your argument list to the ```translate``` block has only one item, this item is the first matched value from your sentence (or if the query didn't expect any matched value then it's the original query sentence).
|
595
|
+
|
596
|
+
Anyhow - every match marked with your regular expression is passed do the ```translate``` block if there are enough arguments defined.
|
597
|
+
|
598
|
+
```ruby
|
599
|
+
translate /Gimme (\d+) beers!/ do |sentence, count_of_beer|
|
600
|
+
# sentence is original query sentence
|
601
|
+
# count_of_beer is the value matched by (\d+)
|
602
|
+
end
|
603
|
+
```
|
604
|
+
|
605
|
+
**Note:** Any argument passed to the block is passed as String. So you have to take care
|
606
|
+
of type casts.
|
607
|
+
|
608
|
+
To have access to the tape's translation block, a tape has to be assigned to a tape deck.
|
609
|
+
|
610
|
+
```ruby
|
611
|
+
class Pub
|
612
|
+
include Seatbelt::Document
|
613
|
+
include Seatbelt::Tape
|
614
|
+
|
615
|
+
use_tape PubTape
|
616
|
+
end
|
617
|
+
```
|
618
|
+
|
619
|
+
Also possible:
|
620
|
+
|
621
|
+
```ruby
|
622
|
+
Pub.add_tape PubTape
|
623
|
+
```
|
624
|
+
|
625
|
+
Having more than one tape:
|
626
|
+
|
627
|
+
```ruby
|
628
|
+
class Pub
|
629
|
+
include Seatbelt::Document
|
630
|
+
include Seatbelt::Tape
|
631
|
+
|
632
|
+
use_tapes PubTape,
|
633
|
+
AnotherTape
|
634
|
+
end
|
635
|
+
```
|
636
|
+
|
637
|
+
Calling ```Pub.answer("Gimme 4 beers!")``` will call the corrosponding translation block.
|
638
|
+
|
639
|
+
**Calling other tapes from a tape**
|
640
|
+
|
641
|
+
To call another tape or a specific translation of a tape use the ```play_tape``` method within the ```translate```block.
|
642
|
+
|
643
|
+
```ruby
|
644
|
+
class CreditCardTape < Seatbelt::Tape
|
645
|
+
|
646
|
+
translate /Charge the credit card with (\d+) Euro./ do |amount|
|
647
|
+
# do something
|
648
|
+
end
|
649
|
+
|
650
|
+
end
|
651
|
+
|
652
|
+
class PubTape < Seatbelt::Tape
|
653
|
+
|
654
|
+
translate /Gimme (\d+) beers!/ do |sentence, count_of_beer|
|
655
|
+
overall_costs = play_tape(:section => "Want the bill for #{count_of_beer} beer")
|
656
|
+
play_tape(CreditCardTape, :section => "Charge the credit card with #{overall_costs} Euro.")
|
657
|
+
end
|
658
|
+
|
659
|
+
translate /Want the bill for (\d+) beer/ do |beer_amount|
|
660
|
+
costs_of_beer = 2
|
661
|
+
sum = 2 * beer_amount.to_i
|
662
|
+
sum
|
663
|
+
end
|
664
|
+
|
665
|
+
end
|
666
|
+
```
|
667
|
+
|
668
|
+
Note the difference between the two ```play_tape``` calls.
|
669
|
+
|
670
|
+
With the ```tape_deck``` object within your translate block you have access to the associated tape deck class (not instance).
|
671
|
+
|
672
|
+
### Translator
|
673
|
+
|
674
|
+
By knowing what tapes and tape decks are, it's easy to understand what the Translator is doing.
|
675
|
+
|
676
|
+
The Translator takes the query and delegates the query to the responsible model.
|
677
|
+
|
678
|
+
```ruby
|
679
|
+
Translator.tell_me "Hotel: 3 persons want to travel for 10 days beginning at next friday to Finnland."
|
680
|
+
```
|
681
|
+
|
682
|
+
Delegates the query ```3 persons want to travel for 10 days beginning at next friday to Finnland.``` to the ```Hotel``` model.
|
683
|
+
|
684
|
+
The model declaration can be ommitted, if this is done, the query is delegated to the
|
685
|
+
model that is defined in the ```Translator.setup```:
|
686
|
+
|
687
|
+
```ruby
|
688
|
+
Seatbelt::Translator.setup do |c|
|
689
|
+
c.namespace = "Seatbelt::Models::"
|
690
|
+
c.default_model_class = "Offer"
|
691
|
+
end
|
692
|
+
```
|
693
|
+
|
694
|
+
That will call ```Seatbelt::Models::Offer``` if no model declaration is given within a query.
|
695
|
+
|
696
|
+
|
697
|
+
Define your tapes in ```lib/seatbelt/tapes```!
|
698
|
+
|
699
|
+
## Contributing
|
700
|
+
|
701
|
+
1. Fork it
|
702
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
703
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
704
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
705
|
+
5. Create new Pull Request
|