silicon 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +372 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +7 -0
  10. data/bin/setup +8 -0
  11. data/exe/silicon +5 -0
  12. data/lib/silicon.rb +6 -0
  13. data/lib/silicon/app.rb +90 -0
  14. data/lib/silicon/base/handle_errors.rb +16 -0
  15. data/lib/silicon/chain.rb +69 -0
  16. data/lib/silicon/chain_factory.rb +39 -0
  17. data/lib/silicon/config.rb +15 -0
  18. data/lib/silicon/errors/silicon_error.rb +4 -0
  19. data/lib/silicon/errors/syntax_error.rb +6 -0
  20. data/lib/silicon/errors/view_engine_error.rb +6 -0
  21. data/lib/silicon/extensions/hash.rb +18 -0
  22. data/lib/silicon/generators/cli.rb +23 -0
  23. data/lib/silicon/generators/templates/actions/common/handle_errors.rb +2 -0
  24. data/lib/silicon/generators/templates/actions/welcome.tt +9 -0
  25. data/lib/silicon/generators/templates/app.rb +4 -0
  26. data/lib/silicon/generators/templates/app.routes +4 -0
  27. data/lib/silicon/generators/templates/config.ru +8 -0
  28. data/lib/silicon/generators/templates/silicon.yml +7 -0
  29. data/lib/silicon/generators/templates/views/show_welcome.json.jbuilder +1 -0
  30. data/lib/silicon/loaders/dependency_loader.rb +14 -0
  31. data/lib/silicon/loaders/template_loader.rb +20 -0
  32. data/lib/silicon/loaders/type_loader.rb +25 -0
  33. data/lib/silicon/request.rb +65 -0
  34. data/lib/silicon/routing/builder.rb +96 -0
  35. data/lib/silicon/routing/file_reader.rb +27 -0
  36. data/lib/silicon/routing/match.rb +12 -0
  37. data/lib/silicon/routing/matcher.rb +40 -0
  38. data/lib/silicon/routing/parser.rb +25 -0
  39. data/lib/silicon/routing/route.rb +19 -0
  40. data/lib/silicon/routing/routing.rb +11 -0
  41. data/lib/silicon/routing/syntax.rb +28 -0
  42. data/lib/silicon/routing/syntax/action.rb +30 -0
  43. data/lib/silicon/routing/syntax/actions.rb +15 -0
  44. data/lib/silicon/routing/syntax/after_section.rb +19 -0
  45. data/lib/silicon/routing/syntax/before_section.rb +18 -0
  46. data/lib/silicon/routing/syntax/catch_section.rb +15 -0
  47. data/lib/silicon/routing/syntax/command.rb +27 -0
  48. data/lib/silicon/routing/syntax/commands.rb +7 -0
  49. data/lib/silicon/routing/syntax/node.rb +47 -0
  50. data/lib/silicon/routing/syntax/nodes.rb +13 -0
  51. data/lib/silicon/routing/syntax/primitives/arrow.rb +4 -0
  52. data/lib/silicon/routing/syntax/primitives/back_arrow.rb +4 -0
  53. data/lib/silicon/routing/syntax/primitives/eol.rb +4 -0
  54. data/lib/silicon/routing/syntax/primitives/http_status.rb +13 -0
  55. data/lib/silicon/routing/syntax/primitives/http_verb.rb +4 -0
  56. data/lib/silicon/routing/syntax/primitives/indent.rb +4 -0
  57. data/lib/silicon/routing/syntax/primitives/parameter.rb +4 -0
  58. data/lib/silicon/routing/syntax/primitives/path.rb +5 -0
  59. data/lib/silicon/routing/syntax/respond.rb +25 -0
  60. data/lib/silicon/routing/syntax/route.rb +25 -0
  61. data/lib/silicon/routing/syntax/sections.rb +19 -0
  62. data/lib/silicon/routing/syntax/tree_section.rb +11 -0
  63. data/lib/silicon/routing/syntax/view.rb +7 -0
  64. data/lib/silicon/routing/syntax_error_interpreter.rb +28 -0
  65. data/lib/silicon/routing/syntax_grammar.tt +95 -0
  66. data/lib/silicon/template_registry.rb +17 -0
  67. data/lib/silicon/version.rb +3 -0
  68. data/lib/silicon/view_builder.rb +18 -0
  69. data/lib/silicon/view_builder_registry.rb +21 -0
  70. data/lib/silicon/view_builders/json.rb +16 -0
  71. data/lib/silicon/view_factory.rb +21 -0
  72. data/silicon.gemspec +45 -0
  73. metadata +272 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: adab41a6ed0b0a061fa8342724d6c7ddea1ec9ce
4
+ data.tar.gz: bee0524057d03b79906ea0d162963082f9a949fd
5
+ SHA512:
6
+ metadata.gz: f8d7ea151bab27929bb6bce0ba34ce56b54bfa9c7d5a5b73bab7122db39e351cfbec2241f617db8c60816bda171844f9955d7980f1a1ab383dfaec796ff91731
7
+ data.tar.gz: 800c81254ae7a6ee8aea32e56eb3ace062a71d9e215a37c18851a7460584dc3f41727b88aa4634e3dd6df49edba2af3479f36be9bab8ecc2a7629017402086af
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
13
+
14
+ .ruby-gemset
15
+ .ruby-version
16
+ .idea
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.1
5
+ before_install: gem install bundler -v 1.15.4
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in silicon.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Vladimir Kalinkin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,372 @@
1
+ # Silicon
2
+ Silicon is a minimalistic web API framework. Its main idea is to implement an approach
3
+ from Clean Architecture principles - "web is just a delivery mechanism".
4
+ Silicon is build on top of IoC container Hypo and forces to use dependency injection
5
+ approach everywhere.
6
+
7
+ ## Architecture
8
+
9
+ ![Silicon Architecture](https://user-images.githubusercontent.com/4575064/34541424-2b7b6c24-f0e9-11e7-8023-abc0df48c9c4.png)
10
+
11
+ ### Separation of Concerns
12
+
13
+ Model-View-Controller (MVC) now is a sign of bad taste. Everybody who built more or less serious production
14
+ web-application know that controllers become to be fat. Splitting controllers by context can a bit improve
15
+ the situation but it's a visual trick, it still looks like:
16
+
17
+ ```ruby
18
+ class UserController
19
+ def index
20
+ # ...
21
+ end
22
+
23
+ def show
24
+ # ...
25
+ end
26
+
27
+ def update
28
+ # ...
29
+ end
30
+
31
+ # ...
32
+ end
33
+ ```
34
+
35
+ It roughly violates one of main OOP rules - [Single Responsibility Principle](). Somebody can say "controller is only intended
36
+ to control the process of request handling", but it's too large responsibility. Actually it's responsible for creating,
37
+ updating and deleting models, showing views, services invocation and so on. And it's an ideal picture. Usual situation is
38
+ when domain code is located in controllers.
39
+ Silicon replaces regular controllers with a chain of atomic actions. It doesn't protect you to use bad practices but
40
+ allows to easily separate \[controller-\]actions. Every action is a simple brick:
41
+
42
+ ```ruby
43
+ class ShowUsers
44
+ def call
45
+ # ...
46
+ end
47
+ end
48
+
49
+ class UpdateUser
50
+ def call
51
+ # ...
52
+ end
53
+ end
54
+ ```
55
+
56
+ A route can represent much more complicated logic containing several steps. For that purpose Silicon provides an ability
57
+ to chain actions:
58
+
59
+ ```
60
+ -> update_user -> notify_admin -> log_action
61
+ ```
62
+
63
+ Every of enumerated above actions are classes respond to method "call". More details about chains construction are
64
+ described below.
65
+
66
+ ### Router
67
+
68
+ Silicon Router is completely new vision of how to create flexible, lightweight, language-independent DSL for designing
69
+ a schema of web-application routing. Rails, Hanami, Sinatra and others provide more or less visually similar DSL for
70
+ defining routing schema. Their routers use Ruby blocks-based DSL for that. Ruby is a power, Ruby is a weakness:
71
+ for such purpose Ruby is too verbose. An example demonstrating all features:
72
+
73
+ ```
74
+ :receive
75
+ .->
76
+ /auth ->
77
+ :before -> load_current_user
78
+
79
+ /posts ->
80
+ GET -> list_posts
81
+ POST -> create_post
82
+
83
+ $id ->
84
+ :before -> load_post
85
+
86
+ GET -> show_post
87
+ PATCH -> update_post -> :respond =200
88
+ DELETE -> remove_post -> :respond =200
89
+
90
+ /comments ->
91
+ GET -> list_comments
92
+ POST => add_comment => notify_author =* notify_subscribers -> :respond <- comment_plain =201
93
+
94
+ $comment_id ->
95
+ GET -> show_comment -> :respond <- comment
96
+ DELETE -> remove_comment
97
+ :catch -> handle_errors
98
+ ```
99
+
100
+ Routes configuration by default is located in file `app/app.routes`. You can change it's location in `silicon.yml` file.
101
+
102
+ #### Action path
103
+ Routes definition has tree-like structure. There're two root entries - :receive and :catch.
104
+
105
+ `:receive` section describes regular flow of incoming request.
106
+
107
+ `:catch` section defines an action which calls when an error raised somewhere in the regular flow.
108
+
109
+ `:receive` section should start from `.` - root point of the routing.
110
+
111
+ Every line of the definition is a piece of path to target action or chain.
112
+
113
+ Symbols `->` and `<TAB>` emulates directory structure. Configuration demonstrated above can be interpreted like:
114
+ ```
115
+ GET /auth/posts/$id (show_post)
116
+ DELETE /auth/posts/$id/comments/$comment_id (remove_comment)
117
+ ```
118
+ Symbol `$` allows to receive request parameters.
119
+
120
+ #### Action chaining
121
+ As you can see, some routes reference to a chain of actions, like:
122
+
123
+ ```
124
+ -> update_user -> notify_admin -> log_action
125
+ ```
126
+ It means that you can define a number of atomic operations in order to reach request goals.
127
+
128
+ `->` is a simple sequential type of action. Process flow waits until finishing of it's execution before starting the next
129
+ action or making a response.
130
+
131
+ More interesting case:
132
+
133
+ ```
134
+ => add_comment => notify_author =* notify_subscribers
135
+ ```
136
+
137
+ `=>` is a parallel operation. All parallel operations complete before the next sequential (`->`) or ending of the chain.
138
+
139
+ `=*` is an asynchronous operation. It must not be completed before next sequential and even ending of the chain.
140
+
141
+ The time of execution of asynchronous and parallel operations can be limited in config file (by default it's 10 seconds).
142
+
143
+ #### Sending Response
144
+ By default Silicon responds with HTTP status 200 (201 for POST) and empty body. You can define :respond instructions
145
+ for sending custom status and specific response body:
146
+
147
+ ```
148
+ ...
149
+ POST => add_comment => ... -> :respond <- comment_plain =201
150
+ ```
151
+
152
+ Expression `:respond <-` declares a view ("comment_plain") and a status `=` (201). Details about views formatting are described below.
153
+
154
+
155
+ ### Dependency Injection
156
+ Dependency Injection (DI) is a heart of Silicon web-application. Every Silicon action can utilize known variables/entities
157
+ in the application. In example:
158
+
159
+ when you have:
160
+
161
+ ```
162
+ GET /auth/posts/$id (show_post)
163
+ ```
164
+
165
+ you can easily use request parameter:
166
+
167
+ ```ruby
168
+ class ShowPost
169
+ def initialize(id)
170
+ @id = id
171
+ end
172
+
173
+ def call
174
+ # somehow extract a post using @id
175
+ end
176
+ end
177
+ ```
178
+
179
+ Every action returns a result that automatically registers in the container and can be used in further actions:
180
+
181
+ ```
182
+ GET /auth/posts/$id (load_post, show_post)
183
+ ```
184
+
185
+ ```ruby
186
+ class ShowPost
187
+ def initialize(load_post_result)
188
+ @post = load_post_result
189
+ end
190
+ # ...
191
+ end
192
+ ```
193
+
194
+ You can customize the name of action result:
195
+
196
+ ```ruby
197
+ class LoadPost
198
+ #...
199
+ def result_name
200
+ 'post'
201
+ end
202
+ end
203
+ ```
204
+
205
+ and use:
206
+
207
+ ```ruby
208
+ class ShowPost
209
+ def initialize(post)
210
+ @post = post
211
+ end
212
+
213
+ # ...
214
+ end
215
+ ```
216
+
217
+ In order to support another types by DI you need to adjust the configuration `silicon.yml`:
218
+
219
+ ```
220
+ path:
221
+ dependencies:
222
+ - actions # default location
223
+ - services/injectable # additional dependencies location
224
+ ```
225
+
226
+ #### Objects lifetime
227
+ As mentioned before, dependency injection is a heart of Silicon. And as you probably noticed we register dependencies
228
+ for every new request. In order to avoid leaking the memory for request-specific objects Silicon uses Hypo::Scope lifetime
229
+ style for registered objects. Every time when request is ending the dependencies remove from the container.
230
+
231
+ BTW, using Hypo::Scope and it's `finalize` method definition you can implement
232
+ [Unit of Work](https://martinfowler.com/eaaCatalog/unitOfWork.html) pattern.
233
+
234
+ ```ruby
235
+ class DbSession
236
+ include Hypo::Scope
237
+
238
+ def initialize
239
+ @transaction = Transaction.new
240
+ end
241
+
242
+ def finalize
243
+ # unexpected behavior handling is in :catch section implementation
244
+ @transaction.commit
245
+ end
246
+ end
247
+ ```
248
+
249
+ ### Views
250
+ By default Silicon handles only JSON requests using [JBuilder](https://github.com/rails/jbuilder) engine. But you can extend a number of engines using
251
+ method `add_view_builder` in your `app.rb`:
252
+
253
+ ```ruby
254
+ class App < Silicon::App
255
+ def initialize
256
+ super
257
+
258
+ add_view_builder(MyHtmlViewBuilder, 'html')
259
+ end
260
+ end
261
+ ```
262
+
263
+ View templates are located in `views` directory; you can change default location in `silicon.yml`:
264
+ ```
265
+ path:
266
+ views: custom/views/location
267
+ ```
268
+
269
+ In view template you can use any objects registered in the container. In example, you have a chain:
270
+
271
+ ```
272
+ GET /posts/$id
273
+ -> load_user -> load_post -> load_comments -> :respond <- show_post
274
+ ```
275
+
276
+ and it's implementation in Ruby:
277
+
278
+ ```ruby
279
+ class LoadPost
280
+ def initialize(id)
281
+ @id = id
282
+ end
283
+
284
+ def call
285
+ # Not a real ORM, just for the demo.
286
+ # Posts.find(id)
287
+ end
288
+
289
+ def result_name
290
+ 'post'
291
+ end
292
+ end
293
+
294
+ class LoadComments
295
+ def initialize(id)
296
+ @id = id
297
+ end
298
+
299
+ def call
300
+ # Not a real ORM, just for the demo.
301
+ # Comments.where(post: post)
302
+ end
303
+
304
+ def result_name
305
+ # as you probably noticed this annoying action can be replaced
306
+ # with a convention in your own base class for application actions.
307
+ # Also instead of this declaration you can still use default name
308
+ # for actions like 'load_comments_result'.
309
+
310
+ 'comments'
311
+ end
312
+ end
313
+ ```
314
+
315
+ Draw the view:
316
+
317
+ ```ruby
318
+ json.post do
319
+ json.title @post.title
320
+ # ...
321
+
322
+ json.comments @comments do |comment|
323
+ json.message comment.message
324
+ # ...
325
+ end
326
+ end
327
+ ```
328
+
329
+ ## Installation
330
+
331
+ Add this line to your application's Gemfile:
332
+
333
+ ```ruby
334
+ gem 'silicon'
335
+ ```
336
+
337
+ And then execute:
338
+
339
+ $ bundle
340
+
341
+ Or install it yourself as:
342
+
343
+ $ gem install silicon
344
+
345
+
346
+ ## Getting Started
347
+
348
+ ## Development
349
+
350
+ Before making any contributions please make sure you are agree:
351
+
352
+ * 3 lines of code is better than 100 for the same functionality implementation, 0 lines is the best.
353
+ * Keep initial idea as simple as it possible. Plugins for additional functionality are more preferable.
354
+ * Do not use comments for obvious code; if your code is not obvious then try to make it obvious -
355
+ extract method, variable, perform more steps to make it more clear.
356
+
357
+ Usual, but always helpful steps:
358
+
359
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
360
+ To install this gem onto your local machine, run `bundle exec rake install`.
361
+ To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
362
+
363
+ ##
364
+
365
+
366
+ ## Contributing
367
+
368
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cylon-v/silicon.
369
+
370
+ ## License
371
+
372
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'silicon'
5
+
6
+ require 'irb'
7
+ IRB.start(__FILE__)