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
@@ -8,7 +8,7 @@ module Smooth
8
8
  class Cache
9
9
  include Singleton
10
10
 
11
- def method_missing meth, *args, &block
11
+ def method_missing(meth, *args, &block)
12
12
  if defined? ::Rails
13
13
  return ::Rails.cache.send(meth, *args, &block)
14
14
  end
@@ -1,35 +1,131 @@
1
- require 'smooth/ext/mutations'
2
- require 'smooth/command/instrumented'
3
-
4
1
  class Smooth::Command < Mutations::Command
5
2
  include Instrumented
6
3
 
4
+ def self.as(current_user)
5
+ require 'smooth/command/run_proxy' unless defined?(RunProxy)
6
+ RunProxy.new(current_user, self)
7
+ end
8
+
9
+ # DSL Improvements English
10
+ def self.params(*args, &block)
11
+ send(:required, *args, &block)
12
+ end
13
+
14
+ def self.interface(*args, &block)
15
+ send(:required, *args, &block)
16
+ end
17
+
18
+ def self.input_argument_names
19
+ required_inputs.keys + optional_inputs.keys
20
+ end
21
+
22
+ # Commands are aware of who is running them
23
+ attr_accessor :current_user
24
+
7
25
  class_attribute :resource_name,
8
26
  :command_action,
9
- :event_namespace
27
+ :event_namespace,
28
+ :model_class,
29
+ :base_scope,
30
+ :parent_resource
10
31
 
11
- def self.scope setting
12
- @@scope = setting
32
+ def self.base_scope
33
+ @base_scope || :all
13
34
  end
14
35
 
15
- def self.params *args, &block
16
- send(:required, *args, &block)
36
+ def parent_api
37
+ self.class.parent_api
38
+ end
39
+
40
+ def parent_resource
41
+ self.class.parent_resource
42
+ end
43
+
44
+ def self.belongs_to_resource(resource)
45
+ self.parent_resource = resource
46
+ end
47
+
48
+ def self.parent_api
49
+ parent_resource.api
50
+ end
51
+
52
+ # Returns the model scope for this command. If a scope method
53
+ # is set on this command, it will make sure to scope the model
54
+ # by that method. It will pass whatever arguments you pass to scope
55
+ # to the scope method. if you pass no args, and the scope requires one,
56
+ # we will assume the user wants us to pass the current user of the command
57
+ def scope(*args)
58
+ @scope ||= begin
59
+ meth = model_class.send(:method, self.class.base_scope)
60
+
61
+ if meth.arity.abs >= 1
62
+ args.push(current_user) if args.empty?
63
+ model_class.send(self.class.base_scope, *args)
64
+ else
65
+ model_class.send(self.class.base_scope)
66
+ end
67
+ end
68
+ end
69
+
70
+ def scope=(new_scope)
71
+ @scope = new_scope || scope
72
+ end
73
+
74
+ def self.scope(setting = nil)
75
+ self.base_scope = setting if setting
76
+ base_scope || :all
17
77
  end
18
78
 
19
79
  def self.event_namespace
20
- @event_namespace || "#{ command_action }.#{ resource_name.singularize.underscore }".downcase
80
+ @event_namespace || "#{ command_action }.#{ resource_alias }".downcase
21
81
  end
22
82
 
23
- def event_namespace; self.class.event_namespace; end
83
+ def self.resource_alias
84
+ resource_name.singularize.underscore
85
+ end
86
+
87
+ def self.resource_name
88
+ value = @resource_name.to_s
89
+
90
+ if value.empty? && model_class
91
+ value = model_class.to_s
92
+ end
93
+
94
+ value
95
+ end
96
+
97
+ def self.object_path
98
+ resource_name.downcase + '.' + command_action
99
+ end
100
+
101
+ def event_namespace
102
+ self.class.event_namespace
103
+ end
104
+
105
+ def object_path
106
+ self.class.object_path
107
+ end
108
+
109
+ def resource_name
110
+ self.class.resource_name
111
+ end
112
+
113
+ def resource_alias
114
+ self.class.resource_alias
115
+ end
116
+
117
+ def model_class
118
+ self.class.model_class
119
+ end
24
120
 
25
121
  # DSL Hooks
26
122
  #
27
123
  #
28
- def self.configure options, resource=nil
124
+ def self.configure(dsl_config_object, resource = nil)
29
125
  resource ||= Smooth.current_resource
30
- klass = define_or_open(options, resource)
126
+ klass = define_or_open(dsl_config_object, resource)
31
127
 
32
- Array(options.blocks).each do |blk|
128
+ Array(dsl_config_object.blocks).each do |blk|
33
129
  klass.class_eval(&blk)
34
130
  end
35
131
 
@@ -41,15 +137,168 @@ class Smooth::Command < Mutations::Command
41
137
  base = Smooth.command
42
138
 
43
139
  name = options.name.to_s.camelize
44
- klass = "#{ name }#{ resource_name }"
140
+ klass = "#{ name }#{ resource.model_class }".gsub(/\s+/, '')
141
+
142
+ apply_options = lambda do |k|
143
+ k.model_class ||= resource.model_class if resource.model_class
144
+
145
+ k.belongs_to_resource(resource)
146
+
147
+ k.resource_name = resource.name.to_s
148
+ k.command_action = options.name.to_s
149
+ end
45
150
 
46
151
  if command_klass = Object.const_get(klass) rescue nil
47
- return command_klass
152
+ return command_klass.tap(&apply_options)
48
153
  end
49
154
 
50
- Object.const_set(klass, Class.new(base)).tap do |k|
51
- k.resource_name = resource.name.to_s
52
- k.command_action = options.name.to_s
155
+ parent_klass = Class.new(base)
156
+
157
+ begin
158
+ Object.const_set(klass, parent_klass).tap(&apply_options)
159
+ rescue => ex
160
+ puts ex.message
161
+ puts "Error setting #{ klass } #{ base }. klass is a #{ klass.class }"
162
+ end
163
+
164
+ parent_klass
165
+ end
166
+
167
+ # Interface Documentation
168
+ #
169
+ def interface_for(filter)
170
+ self.class.interface_description.filters.send(filter)
171
+ end
172
+
173
+ def self.interface_description
174
+ interface_documentation
175
+ end
176
+
177
+ def self.interface_documentation
178
+ optional_inputs = input_filters.optional_inputs
179
+ required_inputs = input_filters.required_inputs
180
+
181
+ data = {
182
+ required: required_inputs.keys,
183
+ optional: optional_inputs.keys,
184
+ filters: {}
185
+ }
186
+
187
+ blk = lambda do |memo, parts, required|
188
+ key, filter = parts
189
+
190
+ type = filter.class.name[/^Mutations::([a-zA-Z]*)Filter$/, 1].underscore
191
+ options = filter.options.merge(required: required)
192
+
193
+ value = memo[key] = {
194
+ type: type,
195
+ options: options.reject { |_k, v| v.nil? },
196
+ description: input_descriptions[key]
197
+ }
198
+
199
+ if options[:faker]
200
+ value[:example] = Smooth.faker(options[:faker])
201
+ end
202
+
203
+ memo
204
+ end
205
+
206
+ required_inputs.reduce(data[:filters]) do |memo, parts|
207
+ blk.call(memo, parts, true)
208
+ end
209
+
210
+ optional_inputs.reduce(data[:filters]) do |memo, parts|
211
+ blk.call(memo, parts, false)
212
+ end
213
+
214
+ data.to_mash
215
+ end
216
+
217
+ def self.filter_for_param(param)
218
+ optional_inputs[param] || required_inputs[param]
219
+ end
220
+
221
+ def self.filter_options_for_param(param)
222
+ filter_for_param(param).try(:options)
223
+ end
224
+
225
+ def self.response_class
226
+ Smooth::Response
227
+ end
228
+
229
+ # Creates a new instance of the Smooth::Command::Response
230
+ # class in response to a request from the Router. It is
231
+ # assumed that a request object responds to: user, and params
232
+ def self.respond_to_request(request_object, options = {})
233
+ klass = self
234
+
235
+ outcome = options.fetch(:outcome) do
236
+ klass.as(request_object.user).run(request_object.params)
237
+ end
238
+
239
+ response_class.new(outcome, serializer_options).tap do |response|
240
+ response.request_headers = request_object.headers
241
+ response.serializer = find_serializer_for(request_object)
242
+ response.event_namespace = event_namespace
243
+ response.command_action = command_action
244
+ response.current_user = request_object.user
245
+ end
246
+ end
247
+
248
+ def self.find_serializer_for(_request_object)
249
+ resource = Smooth.resource(resource_name)
250
+ resource ||= Smooth.resource("#{ resource_name }".pluralize)
251
+
252
+ # TODO
253
+ # We can make the preferred format something you can
254
+ # specify via a header or parameter. We can also restrict
255
+ # certain serializers from certain policies.
256
+ preferred_format = :default
257
+
258
+ resource.fetch(:serializer, preferred_format)
259
+ end
260
+
261
+ def self.serializer_options
262
+ {}
263
+ end
264
+
265
+ # Allows for defining common execution pattern methods
266
+ # mostly for standard CRUD against scoped models
267
+ def self.execute(execution_pattern = nil, &block)
268
+ send :define_method, :execute, (block || get_execution_pattern(execution_pattern))
269
+ end
270
+
271
+ Patterns = {}
272
+
273
+ Patterns[:update] = lambda do
274
+ scoped_find_object.update_attributes(inputs)
275
+ scoped_find_object
276
+ end
277
+
278
+ Patterns[:create] = lambda do
279
+ scope.create(inputs)
280
+ end
281
+
282
+ Patterns[:destroy] = lambda do
283
+ scoped_find_object.destroy
284
+ scoped_find_object
285
+ end
286
+
287
+ def scoped_find_object
288
+ @scoped_find ||= if scope.respond_to?(:find) && found = (scope.find(id) rescue nil)
289
+ found
290
+ else
291
+ add_error(:id, :not_found, "could not find a #{ resource_name } model with that id")
292
+ end
293
+ end
294
+
295
+ def self.get_execution_pattern(pattern_name)
296
+ if respond_to?("#{ pattern_name }_execution_pattern")
297
+ return method("#{ pattern_name }_execution_pattern").to_proc
298
+ elsif Patterns.key?(pattern_name.to_sym)
299
+ Patterns.fetch(pattern_name.to_sym)
300
+ else
301
+ return method(:execute).to_proc
53
302
  end
54
303
  end
55
304
  end
@@ -0,0 +1,27 @@
1
+ module Smooth
2
+ class Command::AsyncWorker
3
+ unless Smooth.config.async_provider
4
+ fail 'Must specify an async provider. e.g. Sidekiq::Worker on Smooth.config.async_provider'
5
+ end
6
+
7
+ def self.options(*args)
8
+ send(:sidekiq_options, *args) if defined?(Sidekiq)
9
+ end
10
+
11
+ def perform(serialized_payload)
12
+ if hash = memory_store.read(serialized_payload)
13
+ api, object_path, payload = hash.values_at('api', 'object_path', 'payload')
14
+ current_user = payload['current_user'] || hash['current_user']
15
+
16
+ chain = Smooth(api).lookup_object_by(object_path)
17
+ chain = chain.as(current_user) if current_user
18
+
19
+ chain.run(payload)
20
+ end
21
+ end
22
+
23
+ def memory_store
24
+ Smooth.config.memory_store
25
+ end
26
+ end
27
+ end
@@ -29,15 +29,17 @@ module Smooth
29
29
  end
30
30
  end
31
31
 
32
- def run_with_instrumentation
32
+ def run_with_instrumentation(event_prefix = nil)
33
33
  outcome = run_with_outcome
34
34
 
35
+ event_prefix = "#{ event_prefix }." if event_prefix
36
+
35
37
  if outcome.success?
36
38
  result = outcome.result
37
- track_event("#{ event_namespace }", result: result, inputs: inputs)
38
- result
39
+ track_event("#{ event_prefix }#{ event_namespace }", result: result, inputs: inputs, current_user: current_user)
40
+ outcome
39
41
  else
40
- track_event("errors/#{ event_namespace }", errors: outcome.errors, inputs: inputs)
42
+ track_event("errors/#{ event_prefix }#{ event_namespace }", errors: outcome.errors, inputs: inputs, current_user: current_user)
41
43
  outcome
42
44
  end
43
45
  end
@@ -0,0 +1,21 @@
1
+ # Internal class to provide current user awareness to the
2
+ module Smooth
3
+ class Command < Mutations::Command
4
+ class RunProxy
5
+ attr_accessor :current_user, :cmd
6
+
7
+ def initialize(current_user, cmd)
8
+ @current_user = current_user
9
+ @cmd = cmd
10
+ end
11
+
12
+ def run!(*args)
13
+ cmd.new(*args).tap { |c| c.current_user = current_user }.run!
14
+ end
15
+
16
+ def run(*args)
17
+ cmd.new(*args).tap { |c| c.current_user = current_user }.run
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,3 @@
1
- require 'singleton'
2
-
3
1
  module Smooth
4
2
  class Configuration
5
3
  include Singleton
@@ -7,18 +5,75 @@ module Smooth
7
5
  cattr_accessor :query_class,
8
6
  :command_class,
9
7
  :serializer_class,
10
- :enable_events
8
+ :object_path_separator,
9
+ :enable_events,
10
+ :definition_folders,
11
+ :eager_load_app_folders,
12
+ :active_record_config,
13
+ :models_path,
14
+ :schema_file,
15
+ :migrations_path,
16
+ :root,
17
+ :include_root_in_json,
18
+ :auth_token_column,
19
+ :enable_factories,
20
+ :async_provider,
21
+ :memory_store,
22
+ :embed_relationships_as
23
+
24
+ @@query_class = Smooth::Query
25
+ @@command_class = Smooth::Command
26
+ @@serializer_class = defined?(ApplicationSerializer) ? ApplicationSerializer : Smooth::Serializer
27
+ @@enable_events = true
28
+ @@eager_load_app_folders = true
29
+ @@models_path = 'app/models'
30
+ @@object_path_separator = '.'
31
+ @@definition_folders = %w(app/models app/apis app/queries app/commands app/serializers app/resources)
32
+ @@include_root_in_json = true
33
+ @@enable_factories = true
11
34
 
12
- @@query_class = Smooth::Query
13
- @@command_class = Smooth::Command
14
- @@serializer_class = defined?(ApplicationSerializer) ? ApplicationSerializer : Smooth::Serializer
15
- @@enable_events = true
35
+ @@active_record_config = 'config/database.yml'
36
+ @@schema_file = 'db/schema.rb'
37
+ @@migrations_path = 'db/migrate'
38
+ @@root = Dir.pwd
39
+ @@auth_token_column = :authentication_token
40
+ @@async_provider = Sidekiq::Worker if defined?(Sidekiq)
41
+ @@memory_store = Smooth.cache
42
+
43
+ @@embed_relationships_as = :ids
44
+
45
+ def active_record
46
+ return active_record_config if active_record_config.is_a?(Hash)
47
+ file = root.join(active_record_config)
48
+ fail 'The config file does not exist at ' + file.to_s unless file.exist?
49
+ YAML.load(file.open).fetch(Smooth.env)
50
+ end
16
51
 
17
52
  def enable_event_tracking?
18
53
  !!@@enable_events
19
54
  end
20
55
 
21
- def self.method_missing meth, *args, &block
56
+ def root
57
+ Pathname(@@root)
58
+ end
59
+
60
+ def app_folder_paths
61
+ Array(definition_folders).map { |f| root.join(f) }
62
+ end
63
+
64
+ def models_path
65
+ root.join(@@models_path)
66
+ end
67
+
68
+ def method_missing(meth, *args, &block)
69
+ if meth.to_s.match(/(\w+)\?$/)
70
+ !!(send(Regexp.last_match[1], *args, &block)) if respond_to?(Regexp.last_match[1])
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+ def self.method_missing(meth, *args, &block)
22
77
  instance.send(meth, *args, &block)
23
78
  end
24
79
  end