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,217 @@
1
+ module Smooth
2
+ class Resource
3
+ class Router
4
+ attr_reader :resource,
5
+ :table,
6
+ :descriptions,
7
+ :rules
8
+
9
+ def initialize(resource, _options = {})
10
+ @resource = resource
11
+ @table = {}
12
+ @descriptions = {}
13
+ @rules = []
14
+ end
15
+
16
+ # I may be getting this in a convoluted way
17
+ # may be easier to build up naturally
18
+ def interface_documentation
19
+ descriptions.keys.reduce({}) do |memo, verb|
20
+ routes = descriptions[verb]
21
+ routes.each do |_|
22
+ pattern, description = _
23
+ memo["#{ verb.to_s.upcase } #{ pattern }"] = description
24
+ end
25
+
26
+ memo
27
+ end
28
+ end
29
+
30
+ def route_table
31
+ @route_table ||= route_patterns_table.reduce({}) do |memo, p|
32
+ route_name, details = p
33
+ memo[route_name] = details[:pattern]
34
+ memo
35
+ end
36
+ end
37
+
38
+ def expand_routes(from_attributes = {})
39
+ route_patterns_table.reduce({}) do |memo, p|
40
+ route_name, details = p
41
+ memo[route_name] = Smooth.util.expand_url_template(details[:template], from_attributes)
42
+ memo
43
+ end
44
+ end
45
+
46
+ def route_patterns_table
47
+ return @route_patterns_table if @route_patterns_table
48
+
49
+ @route_patterns_table = rules.flatten.compact.reduce({}) do |memo, rule|
50
+ memo.tap do
51
+ name = rule[:name]
52
+ pattern = rule[:pattern]
53
+ template = rule[:template]
54
+
55
+ memo[name] = {
56
+ pattern: pattern,
57
+ template: template,
58
+ variables: Array(template.variables)
59
+ }
60
+ end
61
+ end
62
+ end
63
+
64
+ def patterns
65
+ rules.flatten.compact.map { |r| r.fetch(:pattern) }
66
+ end
67
+
68
+ def uri_templates
69
+ rules.flatten.compact.map { |r| r.fetch(:template) }
70
+ end
71
+
72
+ def apply_to(sinatra)
73
+ router = self
74
+
75
+ user_finder = resource.api.method(:lookup_current_user).to_proc
76
+ policy_finder = resource.api.method(:lookup_policy).to_proc
77
+
78
+ router.rules.each do |_|
79
+ options, _ = _
80
+
81
+ handler = methods_table.method(options[:name])
82
+
83
+ sinatra.send options[:method], options[:pattern] do |*args|
84
+ begin
85
+ request = {
86
+ headers: headers,
87
+ params: params,
88
+ user: user_finder.call(params, headers),
89
+ policy: policy_finder.call(params, headers),
90
+ args: args
91
+ }
92
+ rescue => exception
93
+ halt 500, {}, { error: exception.message, backtrace: exception.backtrace, stage: 'request' }.to_json
94
+ end
95
+
96
+ begin
97
+ response = handler.call(request.to_mash)
98
+
99
+ body response.body
100
+ headers response.headers
101
+ status response.status
102
+ rescue => exception
103
+ halt 500, {}, { error: exception.message, backtrace: exception.backtrace, stage: 'response' }.to_json
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ def build_methods_table
110
+ router = self
111
+
112
+ @methods_table_class = Class.new do
113
+
114
+ k = self
115
+
116
+ router.rules.each do |_|
117
+ options, block = _
118
+ method_name = options.fetch(:name)
119
+ k.send :define_method, method_name, (block || router.lookup_handler_for(options[:method], options[:to]))
120
+ end
121
+ end
122
+ end
123
+
124
+ def counter
125
+ @counter ||= 0
126
+ @counter += 1
127
+ end
128
+
129
+ def methods_table
130
+ @methods_table ||= (@methods_table_class || build_methods_table).new
131
+ end
132
+
133
+ def desc(description, *_args)
134
+ descriptions[:current] = description
135
+ end
136
+
137
+ Verbs = {
138
+ get: :get,
139
+ show: :get,
140
+ put: :put,
141
+ patch: :put,
142
+ create: :post,
143
+ delete: :delete,
144
+ destroy: :destroy,
145
+ options: :options,
146
+ post: :post
147
+ }
148
+
149
+ def method_missing(meth, *args, &block)
150
+ if Verbs.keys.include?(meth.to_sym)
151
+ pattern = args.shift
152
+ define_route(meth, pattern, *args, &block)
153
+ else
154
+ super
155
+ end
156
+ end
157
+
158
+ def define_route(request_method, route_pattern, *args, &block)
159
+ request_method = Verbs.fetch(request_method.to_sym, :get)
160
+ bucket = table[request_method] ||= {}
161
+ options = args.extract_options!
162
+
163
+ name = options.fetch(:as, "#{ request_method }_#{ counter }")
164
+
165
+ describe_route(request_method, route_pattern)
166
+
167
+ rules << bucket[route_pattern] = [
168
+ options.merge(name: name, method: request_method, args: args, pattern: route_pattern, template: Smooth.util.uri_template(route_pattern)),
169
+ block
170
+ ]
171
+ end
172
+
173
+ def describe_route(request_method, route_pattern)
174
+ documentation = descriptions[request_method] ||= {}
175
+
176
+ if description = descriptions[:current]
177
+ documentation[route_pattern] = description
178
+ descriptions.delete(:current)
179
+ end
180
+ end
181
+
182
+ # Allows for a configuration syntax like
183
+ #
184
+ # routes do
185
+ # get "/books", :to => :query
186
+ # end
187
+ #
188
+ # the lookup_handler_for method will attempt to
189
+ # discern which object is best suited to handle the
190
+ # request based on the http verb and the signifier
191
+ def lookup_handler_for(method, signifier)
192
+ method = method.to_sym
193
+ signifier = signifier.to_sym
194
+
195
+ resource = self.resource
196
+
197
+ case
198
+
199
+ when method == :get && signifier == :query
200
+ ->(req) { resource.fetch(:query, :default).respond_to_request(req) }
201
+
202
+ when (method == :show || method == :get) && signifier == :show
203
+ ->(req) { resource.fetch(:query, :default).respond_to_find_request(req) }
204
+
205
+ when method == :get
206
+ ->(req) { resource.fetch(:query, signifier).respond_to_request(req) }
207
+
208
+ # Mutation Methods
209
+ when method == :put || method == :post || method == :delete
210
+ ->(req) { resource.fetch(:command, signifier).respond_to_request(req) }
211
+ else
212
+ ->(req) { Smooth::ErrorResponse.new('Unable to find matching route', req) }
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,62 @@
1
+ module Smooth
2
+ class Resource
3
+ module Templating
4
+ FakerGroups = %w(
5
+ Address Lorem Color Company Food HipterIpsum Internet Job Name Movie PhoneNumber Product Unit Vehicle Venue Skill
6
+ )
7
+
8
+ def self.fakers
9
+ FakerGroups.flat_map do |group|
10
+ prefix = group.to_s.underscore.downcase
11
+ space = Faker.const_get(group.to_sym) rescue nil
12
+
13
+ if space && space.class == Module
14
+ (space.methods - Object.methods - [:k, :underscore]).map { |m| "#{prefix}.#{m}" }
15
+ end
16
+ end.compact.uniq
17
+ end
18
+
19
+ def create_from_template(name = nil, *args, &block)
20
+ if name.is_a?(Hash)
21
+ args.unshift(name)
22
+ name = @template_name
23
+ end
24
+ FactoryGirl.create(name || @template_name, *args, &block)
25
+ end
26
+
27
+ def build_from_template(name = nil, *args, &block)
28
+ if name.is_a?(Hash)
29
+ args.unshift(name)
30
+ name = @template_name
31
+ end
32
+
33
+ FactoryGirl.build(name || @template_name, *args, &block)
34
+ end
35
+
36
+ def template(name = nil, *args, &block)
37
+ options = args.extract_options!
38
+
39
+ if name.nil?
40
+ name = model_class.table_name.singularize.to_sym
41
+ @template_name ||= name
42
+ end
43
+
44
+ options[:class] ||= model_class
45
+
46
+ FactoryGirl.define do
47
+ factory(name.to_sym, options, &block)
48
+ end
49
+ end
50
+
51
+ def template_registered?(name = nil)
52
+ name ||= model_class.table_name.singularize.to_sym
53
+ !!(FactoryGirl.factory_by_name(name) rescue nil)
54
+ end
55
+
56
+ # Just allows us to wrap template definitions
57
+ def templates(&block)
58
+ instance_eval(&block)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -5,7 +5,7 @@ module Smooth
5
5
  @@resources ||= {}
6
6
  end
7
7
 
8
- def current_resource= resource_object
8
+ def current_resource=(resource_object)
9
9
  @current_resource = resource_object.identifier
10
10
  end
11
11
 
@@ -0,0 +1,73 @@
1
+ module Smooth
2
+ class Response
3
+ attr_reader :outcome, :serializer_options
4
+
5
+ attr_accessor :request_headers, :serializer, :event_namespace, :command_action, :success, :object, :serializer_klass, :current_user
6
+
7
+ def initialize(outcome, serializer_options = {})
8
+ @outcome = outcome
9
+ @serializer_options = serializer_options
10
+ end
11
+
12
+ def to_rack
13
+ [status, headers, [body]]
14
+ end
15
+
16
+ def headers
17
+ {
18
+ 'Content-Type' => 'application/json'
19
+ }
20
+ end
21
+
22
+ def options
23
+ if success? && serializer
24
+ (@serializer_options || {}).merge(serializer: serializer, scope: current_user)
25
+ else
26
+ @serializer_options.merge(scope: current_user)
27
+ end
28
+ end
29
+
30
+ def body
31
+ serializer.new(object, options).to_json(options)
32
+ end
33
+
34
+ def object
35
+ @object || begin
36
+ if success?
37
+ outcome.result
38
+ else
39
+ outcome.errors.message
40
+ end
41
+ end
42
+ end
43
+
44
+ def success?
45
+ @success || (outcome && outcome.success?)
46
+ end
47
+
48
+ def status
49
+ case
50
+ when success?
51
+ 200
52
+ else
53
+ 400
54
+ end
55
+ end
56
+ end
57
+
58
+ class ErrorResponse < Response
59
+ def initialize(error_message, _request_object, *_args)
60
+ @error_message = error_message
61
+ end
62
+
63
+ def success?
64
+ false
65
+ end
66
+
67
+ def body
68
+ {
69
+ error: error_message
70
+ }
71
+ end
72
+ end
73
+ end
@@ -3,12 +3,50 @@ module Smooth
3
3
  include Smooth::Documentation
4
4
 
5
5
  class_attribute :attribute_descriptions,
6
- :relationship_descriptions
6
+ :relationship_descriptions,
7
+ :resource_route_variables
7
8
 
8
9
  self.attribute_descriptions = {}.to_mash
9
10
  self.relationship_descriptions = {}.to_mash
10
11
 
11
- def self.method_added method_name
12
+ # WIP
13
+ # Need to determine how to access the serialized version
14
+ # as it exists in the context of the serializer instance
15
+ def expand_routes(*slice)
16
+ expanded = parent_resource.expand_routes(route_variables)
17
+
18
+ unless slice.empty?
19
+ expanded = expanded.send(:slice, *slice)
20
+ end
21
+
22
+ expanded.transform_keys do |key|
23
+ "#{ key }_url"
24
+ end
25
+ end
26
+
27
+ def route_variables
28
+ serializer = self
29
+
30
+ self.class.route_variables.reduce({}) do |memo, var|
31
+ value = case
32
+ when serializer.respond_to?(var)
33
+ serializer.send(var)
34
+ when serializer.object.respond_to?(var)
35
+ serializer.object.send(var)
36
+ else
37
+ serializer.read_attribute_for_serialization(var)
38
+ end
39
+
40
+ memo[var] = value
41
+ memo
42
+ end
43
+ end
44
+
45
+ def self.route_variables
46
+ @resource_route_variables ||= parent_resource.router.route_patterns_table.map { |p| _, h = p; h[:variables] }.flatten.compact.uniq
47
+ end
48
+
49
+ def self.method_added(method_name)
12
50
  if documented = inline_description
13
51
  attribute_descriptions[method_name.to_sym] = documented
14
52
  end
@@ -22,7 +60,7 @@ module Smooth
22
60
  schema[:associations]
23
61
  end
24
62
 
25
- def self.configure options, resource=nil
63
+ def self.configure(options, resource = nil)
26
64
  resource ||= Smooth.current_resource
27
65
  klass = define_or_open(options, resource)
28
66
 
@@ -38,22 +76,53 @@ module Smooth
38
76
  base = Smooth.serializer
39
77
 
40
78
  name = options.name
41
- name = nil if name == "Default"
79
+ name = nil if name == 'Default'
80
+
81
+ klass = "#{ resource.model_class }#{ name }".singularize + 'Serializer'
42
82
 
43
- klass = "#{ resource_name }#{ name }".singularize + "Serializer"
83
+ klass = klass.gsub(/\s+/, '')
44
84
 
45
85
  if serializer_klass = Object.const_get(klass) rescue nil
46
86
  return serializer_klass
47
87
  end
48
88
 
49
- Object.const_set(klass, Class.new(base))
89
+ parent_klass = Class.new(base)
90
+
91
+ parent_klass.belongs_to_resource(resource)
92
+
93
+ begin
94
+ Object.const_set(klass, parent_klass)
95
+ rescue => ex
96
+ puts ex.message
97
+ puts "Error setting #{ klass } #{ base }. klass is a #{ klass.class }"
98
+ end
99
+
100
+ parent_klass
50
101
  end
51
102
 
52
- def self.documentation_for_attribute attribute
103
+ class_attribute :parent_resource
104
+
105
+ def parent_resource
106
+ self.class.parent_resource
107
+ end
108
+
109
+ def parent_api
110
+ self.class.parent_api
111
+ end
112
+
113
+ def self.belongs_to_resource(resource)
114
+ self.parent_resource = resource
115
+ end
116
+
117
+ def self.parent_api
118
+ parent_resource.api
119
+ end
120
+
121
+ def self.documentation_for_attribute(attribute)
53
122
  attribute_descriptions[attribute.to_sym]
54
123
  end
55
124
 
56
- def self.documentation_for_association association
125
+ def self.documentation_for_association(association)
57
126
  relationship_descriptions[association.to_sym]
58
127
  end
59
128
 
@@ -61,7 +130,11 @@ module Smooth
61
130
  attribute_descriptions.merge(relationship_descriptions).to_mash
62
131
  end
63
132
 
64
- def self.attribute attr, options={}
133
+ def self.interface_documentation
134
+ documentation
135
+ end
136
+
137
+ def self.attribute(attr, options = {})
65
138
  documented = inline_description
66
139
 
67
140
  if documented
@@ -71,7 +144,13 @@ module Smooth
71
144
  super
72
145
  end
73
146
 
74
- def self.has_one attr, options={}
147
+ def self.computed(*args, &block)
148
+ property_name = args.first
149
+ send(:define_method, property_name, &block)
150
+ send(:attribute, *args)
151
+ end
152
+
153
+ def self.has_one(attr, options = {})
75
154
  documented = inline_description
76
155
 
77
156
  if documented
@@ -81,7 +160,7 @@ module Smooth
81
160
  super
82
161
  end
83
162
 
84
- def self.has_many attr, options={}
163
+ def self.has_many(attr, options = {})
85
164
  documented = inline_description
86
165
 
87
166
  if documented
@@ -90,5 +169,17 @@ module Smooth
90
169
 
91
170
  super
92
171
  end
172
+
173
+ def self.return_ids_for_relationships!
174
+ @returns_ids_for_relationships = true
175
+ embed :ids
176
+ end
177
+
178
+ def self.returns_ids_for_relationships?
179
+ @returns_ids_for_relationships == true
180
+ end
181
+ end
182
+
183
+ class ArraySerializer < ActiveModel::ArraySerializer
93
184
  end
94
185
  end