smooth 2.0.1 → 2.0.2

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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -0
  3. data/Gemfile +1 -2
  4. data/README.md +150 -5
  5. data/Rakefile +16 -0
  6. data/app/assets/javascripts/smooth/index.js +5152 -0
  7. data/bin/smooth +9 -0
  8. data/{app/assets/javascripts/smooth → developer-tools}/.keep +0 -0
  9. data/developer-tools/bower.json +8 -0
  10. data/developer-tools/config.ru +3 -0
  11. data/developer-tools/dist/08d606864d3ad3f0b98660d391f5a1c2.gif +0 -0
  12. data/developer-tools/dist/2d66bcdc27cd89f71068e98a7a929712.gif +0 -0
  13. data/developer-tools/dist/3e9816417b11485d454f9b3662b06e7b.eot +0 -0
  14. data/developer-tools/dist/47de617fd1d745ad120ccb9e2924b98c.gif +0 -0
  15. data/developer-tools/dist/5ae23ad29b67289a1375d2043e289c52.eot +0 -0
  16. data/developer-tools/dist/60c2a8500e63bf211b7df9608f7613ea.svg +450 -0
  17. data/developer-tools/dist/645f50ba6c1e56f078fa018855d97eb0.gif +0 -0
  18. data/developer-tools/dist/71ab514d1cedda303417ad7a06472fea.ttf +0 -0
  19. data/developer-tools/dist/8cca2f02b0af2da365ff4d1755f29146.ttf +0 -0
  20. data/developer-tools/dist/939cf252f0eb4efbd2d170c974411c49.gif +0 -0
  21. data/developer-tools/dist/9af25aaeb6ca6d08d213b04841813eb5.gif +0 -0
  22. data/developer-tools/dist/b683029bafe0305ac2234038a03e1541.woff +0 -0
  23. data/developer-tools/dist/c9dec22105ad9330c811599b8b6464f8.woff +0 -0
  24. data/developer-tools/dist/ca279c55a51ab2641c4712a333633581.gif +0 -0
  25. data/developer-tools/dist/client.js +5152 -0
  26. data/developer-tools/dist/f5b27137d3f5e9b1d91b16b37386dd03.gif +0 -0
  27. data/developer-tools/dist/f99a231ed57ee113b50b1c3e9f9fcdc3.svg +399 -0
  28. data/developer-tools/dist/index.html +18 -0
  29. data/developer-tools/dist/inspector.js +38432 -0
  30. data/developer-tools/dist/jquery.min.js +9190 -0
  31. data/developer-tools/package.json +39 -0
  32. data/developer-tools/server.js +14 -0
  33. data/developer-tools/src/client.coffee +21 -0
  34. data/developer-tools/src/client/collection.coffee +14 -0
  35. data/developer-tools/src/client/model.coffee +11 -0
  36. data/developer-tools/src/client/resource.coffee +132 -0
  37. data/{app/controllers/.keep → developer-tools/src/client/runner.coffee} +0 -0
  38. data/developer-tools/src/dependencies.coffee +7 -0
  39. data/developer-tools/src/inspector.cjsx +49 -0
  40. data/developer-tools/src/inspector/models/interface_collection.coffee +31 -0
  41. data/developer-tools/src/inspector/pages/index.cjsx +31 -0
  42. data/developer-tools/src/inspector/pages/resources.cjsx +5 -0
  43. data/developer-tools/src/inspector/views/grid_sort.cjsx +23 -0
  44. data/developer-tools/src/inspector/views/icon_heading.cjsx +15 -0
  45. data/developer-tools/src/inspector/views/resource_card.cjsx +34 -0
  46. data/developer-tools/src/inspector/views/sidebar.cjsx +12 -0
  47. data/developer-tools/src/inspector/views/toolbar.cjsx +17 -0
  48. data/developer-tools/src/styles/index.scss +136 -0
  49. data/developer-tools/src/styles/views.scss +13 -0
  50. data/developer-tools/src/util.coffee +48 -0
  51. data/developer-tools/webpack.config.js +56 -0
  52. data/developer-tools/webpack.hot.config.js +65 -0
  53. data/lib/smooth.rb +209 -28
  54. data/lib/smooth/active_record/adapter.rb +24 -0
  55. data/lib/smooth/api.rb +272 -18
  56. data/lib/smooth/api/policy.rb +2 -2
  57. data/lib/smooth/api/tracking.rb +4 -4
  58. data/lib/smooth/application.rb +66 -0
  59. data/lib/smooth/cache.rb +1 -1
  60. data/lib/smooth/command.rb +267 -18
  61. data/lib/smooth/command/async_worker.rb +27 -0
  62. data/lib/smooth/command/instrumented.rb +6 -4
  63. data/lib/smooth/command/run_proxy.rb +21 -0
  64. data/lib/smooth/configuration.rb +63 -8
  65. data/lib/smooth/documentation.rb +3 -6
  66. data/lib/smooth/dsl.rb +1 -36
  67. data/lib/smooth/dsl_adapter.rb +34 -0
  68. data/lib/smooth/event.rb +8 -4
  69. data/lib/smooth/event/proxy.rb +9 -0
  70. data/lib/smooth/event/relay.rb +38 -0
  71. data/lib/smooth/example.rb +1 -1
  72. data/lib/smooth/ext/core.rb +16 -0
  73. data/lib/smooth/model_adapter.rb +31 -0
  74. data/lib/smooth/query.rb +143 -13
  75. data/lib/smooth/resource.rb +227 -52
  76. data/lib/smooth/resource/router.rb +217 -0
  77. data/lib/smooth/resource/templating.rb +62 -0
  78. data/lib/smooth/resource/tracking.rb +1 -1
  79. data/lib/smooth/response.rb +73 -0
  80. data/lib/smooth/serializer.rb +102 -11
  81. data/lib/smooth/user_adapter.rb +83 -0
  82. data/lib/smooth/util.rb +17 -0
  83. data/lib/smooth/version.rb +1 -1
  84. data/smooth.gemspec +6 -2
  85. data/spec/acceptance/books_routes_spec.rb +50 -0
  86. data/spec/acceptance/embedded_relationships_spec.rb +26 -0
  87. data/spec/dummy/app/apis/application_api.rb +8 -3
  88. data/spec/dummy/app/commands/create_book.rb +5 -0
  89. data/spec/dummy/app/models/book.rb +1 -0
  90. data/spec/dummy/app/models/library.rb +2 -0
  91. data/spec/dummy/app/models/user.rb +2 -0
  92. data/spec/dummy/app/queries/book_query.rb +13 -0
  93. data/spec/dummy/app/resources/{books.rb → books_definition.rb} +37 -12
  94. data/spec/dummy/db/migrate/20140824215902_create_users.rb +10 -0
  95. data/spec/dummy/db/migrate/20140826193259_create_libraries.rb +10 -0
  96. data/spec/dummy/db/schema.rb +8 -1
  97. data/spec/lib/smooth/api/async_spec.rb +21 -0
  98. data/spec/lib/smooth/api_spec.rb +8 -0
  99. data/spec/lib/smooth/command_spec.rb +87 -6
  100. data/spec/lib/smooth/configuration_spec.rb +4 -0
  101. data/spec/lib/smooth/event/relay_spec.rb +33 -0
  102. data/spec/lib/smooth/event_spec.rb +5 -8
  103. data/spec/lib/smooth/query_spec.rb +42 -0
  104. data/spec/lib/smooth/resource/router_spec.rb +14 -0
  105. data/spec/lib/smooth/resource_spec.rb +33 -1
  106. data/spec/lib/smooth/serializer_spec.rb +20 -0
  107. data/spec/lib/smooth/templating_spec.rb +23 -0
  108. data/spec/lib/smooth/util_spec.rb +22 -0
  109. data/spec/spec_helper.rb +1 -1
  110. metadata +151 -17
  111. data/app/helpers/.keep +0 -0
  112. data/app/mailers/.keep +0 -0
  113. data/app/models/.keep +0 -0
  114. data/app/views/.keep +0 -0
  115. data/spec/dummy/db/development.sqlite3 +0 -0
  116. data/spec/dummy/db/test.sqlite3 +0 -0
@@ -0,0 +1,24 @@
1
+ module Smooth
2
+ module AR
3
+ class Adapter
4
+ class << self
5
+ def configure
6
+ end
7
+
8
+ def in_use?
9
+ connection.in_use?
10
+ rescue ActiveRecord::ConnectionNotEstablished
11
+ false
12
+ end
13
+
14
+ def connection
15
+ ActiveRecord::Base.connection
16
+ end
17
+
18
+ def establish_connection
19
+ @connection = ActiveRecord::Base.establish_connection(Smooth.config.active_record)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,11 +1,21 @@
1
- require 'smooth/dsl'
2
-
1
+ # The Smooth API
2
+ #
3
+ # An API is a collection of resources. A Resource is a collection of data models, and the API
4
+ # allows us to run queries against those data models or to run commands that express an intent
5
+ # to mutate the data models.
6
+ #
7
+ # An API provides different methods of authentication, and different policies for authorizations.
8
+ #
9
+ # An API is very easy to put a rest interface in front of, but can also work in other scenarios
10
+ # that speak JSON since the interface pretty well encapsulates the behavior.
3
11
  module Smooth
4
12
  class Api
13
+ # Being able to inspect an API and produce data suitable for generating interface
14
+ # documentation, and automated tests, among other things, is a key feature of the gem.
5
15
  include Smooth::Documentation
6
16
 
7
17
  def self.default
8
- @default ||= Smooth::Api.new
18
+ @default ||= Smooth::Api.new(:default)
9
19
  end
10
20
 
11
21
  attr_accessor :name,
@@ -13,20 +23,231 @@ module Smooth
13
23
  :_resources,
14
24
  :_policies
15
25
 
16
- def initialize(name, options={})
17
- @name = name
26
+ def initialize(name, options = {})
27
+ @name = name.to_s
18
28
  @options = options
19
29
 
20
30
  @_resources = {}
21
31
  @_policies = {}
22
32
  end
23
33
 
24
- def version config=nil
34
+ # The Smooth API is a gateway for the commands and queries
35
+ # that can be run by users against its resources
36
+ def lookup(path)
37
+ lookup_object_by(path)
38
+ end
39
+
40
+ # The Smooth API Provides a Rack Compatible interface
41
+ # so we can mount in sinatra or rails or whatever
42
+ def call(env)
43
+ sinatra.call(env)
44
+ end
45
+
46
+ # All Actions taken against the Smooth API are run 'as'
47
+ # some current user. Example:
48
+ #
49
+ # Running a command:
50
+ #
51
+ # api.as(jonathan).i_would_like_to
52
+ # .run_command("books.create").with(title:'Sup boo')
53
+ #
54
+ # Running a query
55
+ #
56
+ # api.as(soederpop).i_would_like_to
57
+ # .query("books").with(subject:"how to...")
58
+ #
59
+ def as(current_user, &block)
60
+ proxy = DslProxy.new(current_user, self)
61
+ proxy.instance_eval(&block) if block_given?
62
+ proxy
63
+ end
64
+
65
+ # The Smooth API generates a sinatra app to be able to
66
+ # the various resources and run commands, queries, etc.
67
+ def sinatra
68
+ app = @sinatra_application_klass ||= Class.new(Sinatra::Base)
69
+
70
+ @sinatra ||= begin
71
+ _resources.each do |_name, resource|
72
+ resource.router && resource.router.apply_to(app)
73
+ end
74
+
75
+ expose_interface_documentation_via(app)
76
+
77
+ app
78
+ end
79
+ end
80
+
81
+ def inspect
82
+ "Smooth API: #{ name } Resources: #{ resource_names }"
83
+ end
84
+
85
+ # The API will rely on the configured authentication method
86
+ # to determine who the user is. Given some request params
87
+ # and request headers
88
+ def lookup_current_user(params, headers)
89
+ auth_strategy, key = authentication_strategy
90
+
91
+ case
92
+ when auth_strategy == :param && parts = params[key]
93
+ user_class.find_for_token_authentication(parts)
94
+ when auth_strategy == :header && parts = headers[key]
95
+ user_class.find_for_token_authentication(parts)
96
+ else
97
+ user_class.anonymous(params, headers)
98
+ end
99
+ end
100
+
101
+ # The Policy will provide an ability file that we can
102
+ # run a user though. The Policy can be overridden by the
103
+ # resource, too. A policy will pass an object path
104
+ def lookup_policy(_params, _headers)
105
+ {}.to_mash
106
+ # TODO
107
+ #
108
+ # Implement:
109
+ #
110
+ # I think Smooth replaces too much of cancan to rely on it.
111
+ #
112
+ # I think the model where the resource inherits from the api, and
113
+ # the api policy just white lists or black lists commands for given
114
+ # user roles, will be sufficient
115
+ end
116
+
117
+ # The Smooth API provides an Asynchronous interface.
118
+ def perform_async(object_path, payload = {})
119
+ worker.perform_async serialize_for_async(object_path, payload)
120
+ end
121
+
122
+ # Takes a request to do something and serializes the arguments in
123
+ # the memory store. The request will be dispatched to the background job
124
+ # handler and then resumed with the same arguments.
125
+ #
126
+ # Note: Rails Global ID will be a good replacement for this
127
+ def serialize_for_async(object_path, payload)
128
+ key = "#{ name }".parameterize + ":cmd:#{ String.random_token(16) }"
129
+
130
+ request = {
131
+ api: name,
132
+ object_path: object_path,
133
+ payload: payload
134
+ }
135
+
136
+ Smooth.config.memory_store.write(key, request)
137
+
138
+ key
139
+ end
140
+
141
+ # Look up object by path. Used to route requests to
142
+ # commands or queries.
143
+ #
144
+ # Example:
145
+ #
146
+ # lookup('books.create') #=> CreateBook
147
+ def lookup_object_by(path)
148
+ path = path.to_s
149
+ resource_name, object_name = path.split(Smooth.config.object_path_separator)
150
+
151
+ resource_object = resource(resource_name)
152
+
153
+ case
154
+ when object_name == 'query' || object_name == 'serializer'
155
+ resource_object.fetch(object_name.to_sym, :default)
156
+ when object_name.nil?
157
+ resource_object
158
+ else
159
+ resource_object.fetch(:command, object_name)
160
+ end
161
+ end
162
+
163
+ def resource_keys
164
+ _resources.keys
165
+ end
166
+
167
+ def resource_names
168
+ _resources.values.map(&:resource_name).compact
169
+ end
170
+
171
+ def resource_group_names
172
+ _resources.values.map(&:group_description).compact
173
+ end
174
+
175
+ def documentation_base
176
+ {
177
+ api_meta: {
178
+ resource_names: resource_names,
179
+ resource_groups: resource_group_names
180
+ }
181
+ }
182
+ end
183
+
184
+ def interface_documentation
185
+ resource_keys.reduce(documentation_base) do |memo, key|
186
+ memo.tap do
187
+ if resource = resource(key)
188
+ memo[resource.resource_name || key.to_s] = resource.interface_documentation
189
+ end
190
+ end
191
+ end
192
+ end
193
+
194
+ def expose_interface_documentation_via(sinatra)
195
+ api = self
196
+
197
+ sinatra.send :get, '/interface' do
198
+ api.interface_documentation.to_json
199
+ end
200
+
201
+ sinatra.send :get, '/interface/:resource_name' do
202
+ docs = api.interface_documentation[params[:resource_name]]
203
+ docs.to_json
204
+ end
205
+ end
206
+
207
+ def version(config = nil)
25
208
  @_version_config = config if config
26
209
  @_version_config
27
210
  end
28
211
 
29
- def policy policy_name, options={}, &block
212
+ def user_class(user_klass = nil, &block)
213
+ @user_class = user_klass if user_klass.present?
214
+ @user_class || User
215
+ @user_class.class_eval(&block) if block_given?
216
+ @user_class
217
+ end
218
+
219
+ def authentication_strategy(option = nil, key = nil)
220
+ return @authentication_strategy || [:header, 'X-AUTH-TOKEN'] if option.nil?
221
+
222
+ unless option.nil?
223
+ key = case
224
+ when key.present?
225
+ key
226
+ when option.to_sym == :param
227
+ :auth_token
228
+ when option.to_sym == :header
229
+ 'X-AUTH-TOKEN'
230
+ end
231
+ end
232
+
233
+ @authentication_strategy = [option, key]
234
+ end
235
+
236
+ def worker(&block)
237
+ worker_name = "#{ name }".camelize + 'Worker'
238
+
239
+ if worker_klass = Smooth::Api.const_get(worker_name) rescue nil
240
+ @worker_klass = worker_klass
241
+ else
242
+ Object.const_get(worker_name, @worker_klass = Class.new(Smooth::Command::AsyncWorker))
243
+ end
244
+
245
+ @worker_klass.instance_eval(&block)
246
+
247
+ @worker_klass
248
+ end
249
+
250
+ def policy(policy_name, options = {}, &block)
30
251
  if obj = _policies[policy_name.to_sym]
31
252
  obj.apply_options(options) unless options.empty?
32
253
  obj.instance_eval(&block) if block_given?
@@ -37,22 +258,23 @@ module Smooth
37
258
 
38
259
  elsif block_given?
39
260
  obj = Smooth::Api::Policy.new(policy_name, options, &block)
40
- _resources[policy_name.to_sym] = obj
261
+ _policies[policy_name.to_sym] = obj
41
262
  end
42
263
  end
43
264
 
44
- def has_resource? resource_name
45
- resources.has_key?(resource_name.to_sym)
265
+ def has_resource?(resource_name)
266
+ resources.key?(resource_name.to_sym)
46
267
  end
47
268
 
48
- def resource resource_name, options={}, &block
49
- api_name = self.name
269
+ def resource(resource_name, options = {}, &block)
270
+ api_name = name
50
271
 
51
- if existing = _resources[resource_name.to_sym]
272
+ existing = _resources[resource_name.to_s.downcase]
273
+
274
+ if existing
52
275
  existing.apply_options(options) unless options.empty?
53
276
  existing.instance_eval(&block) if block_given?
54
277
  existing
55
-
56
278
  elsif options.empty? && !block_given?
57
279
  existing = nil
58
280
 
@@ -61,12 +283,44 @@ module Smooth
61
283
  obj.api_name = api_name
62
284
  end
63
285
 
64
- _resources[resource_name.to_sym] = created
286
+ _resources[resource_name.to_s.downcase] = created
65
287
  end
66
288
  end
289
+ end
290
+
291
+ class DslProxy
292
+ def initialize(current_user, api)
293
+ @current_user = current_user
294
+ @api = api
295
+ end
296
+
297
+ def i_would_like_to
298
+ self
299
+ end
67
300
 
301
+ def lemme
302
+ self
303
+ end
304
+
305
+ def imll
306
+ self
307
+ end
308
+
309
+ def query(resource_name, *args)
310
+ params = args.extract_options!
311
+ query_name = args.first || :default
312
+ runner = @api.resource(resource_name).fetch(:query, query_name).as(@current_user)
313
+ runner.async? ? perform_async(runner.object_path, params) : runner.run(params)
314
+ end
315
+
316
+ def run_command(resource_name, *args)
317
+ params = args.extract_options!
318
+ command_name = args.first
319
+ path = resource_name if command_name.nil?
320
+ path = "#{ resource_name }.#{ command_name }" if command_name.present?
321
+
322
+ runner = @api.lookup_object_by(path).as(@current_user)
323
+ runner.async? ? perform_async(runner.object_path, params) : runner.run(params)
324
+ end
68
325
  end
69
326
  end
70
-
71
- require 'smooth/api/tracking'
72
- require 'smooth/api/policy'
@@ -5,12 +5,12 @@ module Smooth
5
5
 
6
6
  attr_accessor :name
7
7
 
8
- def initialize name, options={}
8
+ def initialize(name, options = {})
9
9
  @name = name
10
10
  @options = options
11
11
  end
12
12
 
13
- def apply_options *opts
13
+ def apply_options(*opts)
14
14
  @options.send(:merge!, *opts)
15
15
  end
16
16
  end
@@ -5,7 +5,7 @@ module Smooth
5
5
  @@apis ||= {}
6
6
  end
7
7
 
8
- def fetch_api name, &block
8
+ def fetch_api(name, &block)
9
9
  existing = apis[name.to_sym]
10
10
 
11
11
  if existing.nil? && block_given?
@@ -16,15 +16,15 @@ module Smooth
16
16
  end
17
17
 
18
18
  def current_api
19
- apis[current_api_name] ||= Smooth::Api.default()
19
+ apis[current_api_name] ||= Smooth::Api.default
20
20
  end
21
21
 
22
- def current_api_name= value
22
+ def current_api_name=(value)
23
23
  @@current_api_name = value
24
24
  end
25
25
 
26
26
  def current_api_name
27
- (@@current_api_name || :default).to_sym
27
+ (@@current_api_name ||= :default).to_sym
28
28
  end
29
29
  end
30
30
  end
@@ -0,0 +1,66 @@
1
+ module Smooth
2
+ class Application
3
+ attr_reader :options
4
+
5
+ class User < Hashie::Mash
6
+ end
7
+
8
+ def initialize(options = {}, &block)
9
+ @options = options
10
+ instance_eval(&block) if block_given?
11
+
12
+ config do
13
+ self.root = options[:root]
14
+ end
15
+
16
+ boot unless options[:defer]
17
+ end
18
+
19
+ def config(&block)
20
+ Smooth.config(&block)
21
+ end
22
+
23
+ def console
24
+ require 'pry'
25
+ Pry.start(self, {})
26
+ end
27
+
28
+ def system_user
29
+ @system_user ||= User.new(email: 'system@smooth.io', role: 'system')
30
+ end
31
+
32
+ def api
33
+ @api ||= Smooth(options[:api] || :default)
34
+ end
35
+
36
+ def smooth
37
+ @smooth ||= api.as(system_user)
38
+ end
39
+
40
+ def resource(*args, &_block)
41
+ api.send(:resource, *args)
42
+ end
43
+
44
+ def query(*args)
45
+ smooth.send(:query, *args)
46
+ end
47
+
48
+ def command(*args)
49
+ smooth.send(:run_command, *args)
50
+ end
51
+
52
+ def load_models
53
+ Dir[config.models_path.join('**/*.rb')].each do |f|
54
+ require config.models_path.join(f)
55
+ end
56
+ end
57
+
58
+ def boot
59
+ @boot ||= begin
60
+ Smooth.active_record.establish_connection
61
+ load_models
62
+ Smooth.eager_load_from_app_folders
63
+ end
64
+ end
65
+ end
66
+ end