praxis 0.9 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/CHANGELOG.md +51 -0
  4. data/CONTRIBUTING.md +3 -2
  5. data/Gemfile +1 -1
  6. data/README.md +5 -3
  7. data/lib/api_browser/app/css/main.css +4 -0
  8. data/lib/api_browser/app/js/directives/attribute_description.js +17 -14
  9. data/lib/api_browser/app/js/directives/type_label.js +7 -7
  10. data/lib/api_browser/app/sass/modules/_body.scss +8 -1
  11. data/lib/api_browser/app/views/directives/attribute_description/_example.html +9 -4
  12. data/lib/api_browser/app/views/directives/attribute_table_row/_links.html +4 -3
  13. data/lib/api_browser/app/views/resource/_actions.html +11 -5
  14. data/lib/praxis/action_definition/headers_dsl_compiler.rb +6 -6
  15. data/lib/praxis/action_definition.rb +19 -4
  16. data/lib/praxis/api_definition.rb +3 -3
  17. data/lib/praxis/application.rb +1 -0
  18. data/lib/praxis/bootloader_stages/routing.rb +3 -3
  19. data/lib/praxis/bootloader_stages/warn_unloaded_files.rb +1 -0
  20. data/lib/praxis/callbacks.rb +35 -0
  21. data/lib/praxis/controller.rb +18 -35
  22. data/lib/praxis/file_group.rb +12 -7
  23. data/lib/praxis/links.rb +4 -0
  24. data/lib/praxis/media_type_collection.rb +21 -15
  25. data/lib/praxis/request.rb +2 -7
  26. data/lib/praxis/request_stages/action.rb +10 -7
  27. data/lib/praxis/request_stages/request_stage.rb +45 -10
  28. data/lib/praxis/request_stages/validate.rb +7 -3
  29. data/lib/praxis/responses/http.rb +0 -24
  30. data/lib/praxis/responses/internal_server_error.rb +40 -0
  31. data/lib/praxis/responses/validation_error.rb +1 -0
  32. data/lib/praxis/version.rb +1 -1
  33. data/lib/praxis.rb +5 -24
  34. data/praxis.gemspec +3 -3
  35. data/spec/functional_spec.rb +62 -17
  36. data/spec/praxis/action_definition_spec.rb +14 -6
  37. data/spec/praxis/callbacks_spec.rb +107 -0
  38. data/spec/praxis/controller_spec.rb +0 -38
  39. data/spec/praxis/file_group_spec.rb +5 -0
  40. data/spec/praxis/media_type_collection_spec.rb +20 -7
  41. data/spec/praxis/request_spec.rb +15 -1
  42. data/spec/praxis/request_stage_spec.rb +93 -0
  43. data/spec/praxis/request_stages_action_spec.rb +61 -0
  44. data/spec/praxis/request_stages_validate_spec.rb +1 -1
  45. data/spec/praxis/responses/internal_server_error_spec.rb +51 -0
  46. data/spec/praxis/responses/validation_error_spec.rb +66 -0
  47. data/spec/praxis/router_spec.rb +5 -1
  48. data/spec/spec_app/app/concerns/authenticated.rb +15 -0
  49. data/spec/spec_app/app/concerns/basic_api.rb +12 -0
  50. data/spec/spec_app/app/concerns/log_wrapper.rb +14 -0
  51. data/spec/spec_app/app/controllers/base_class.rb +14 -0
  52. data/spec/spec_app/app/controllers/instances.rb +31 -5
  53. data/spec/spec_app/app/controllers/volumes.rb +17 -0
  54. data/spec/spec_app/config/environment.rb +1 -0
  55. data/spec/spec_app/design/api.rb +3 -3
  56. data/spec/spec_app/design/media_types/volume.rb +1 -1
  57. data/spec/spec_app/design/media_types/volume_snapshot.rb +1 -0
  58. data/spec/spec_app/design/resources/instances.rb +10 -3
  59. data/spec/spec_app/design/resources/volumes.rb +27 -0
  60. data/spec/support/spec_resource_definitions.rb +32 -0
  61. data/tasks/praxis_app_generator.thor +2 -2
  62. metadata +23 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ea496999177f99af52478aa098d74e4eb07756bf
4
- data.tar.gz: acbe97318d3f8951ea9e340ef7fe6b243162450a
3
+ metadata.gz: 8ef88025b412d1a511b45aaeebc4da3dc77e26ba
4
+ data.tar.gz: 12055f1821e6dbb2bf2db0db9243cc41c49ca8ab
5
5
  SHA512:
6
- metadata.gz: a2a86df947317e255b79e44867424a18466031aef0978a457acc21292c2f7d33360dee2812b982467a3a964ad7e5de64e005f5cf501f8df8faa17bf1aae95893
7
- data.tar.gz: 866fc777e303b9aef3b1cafa85b791ef1612daf7b345296fe8296735f135ed7529fc6542104fefdd3318f06bc9805c0079c14ab9911a3aefbdbd7b96a12ee48f
6
+ metadata.gz: 3976e3f0b241efbad32a3b1b1ff65b0aaad3c9b6e6c7d38e00f611441f2f9c4ee2a75ea4c389cbe7a055795a6e8da0713d191f1d142310beaf7c4ca9a88265d5
7
+ data.tar.gz: 8f609d4f6704949f59f6ab8fc35a9319909cec4ed90275abf03f2103fa1ddd11fb851a21a01fbf98ee00a4c6ad30b1d52542e33804ff94cb6957a532327b4468
data/.travis.yml CHANGED
@@ -1,4 +1,5 @@
1
1
  language: ruby
2
+ cache: bundler
2
3
  rvm:
3
4
  - "2.1.2"
4
5
  script: bundle exec rspec spec
data/CHANGELOG.md ADDED
@@ -0,0 +1,51 @@
1
+ # praxis changelog
2
+
3
+ ## next
4
+
5
+ * next thing here
6
+
7
+ ## 0.10.0
8
+
9
+ * Avoid loading responses (and templates) lazily as they need to be registered in time
10
+ * Fix: app generator's handling of 404. [@magneland](https://github.com/magneland) [Issue #10](https://github.com/rightscale/praxis/issues/10)
11
+ * Fix: Getting started doc. [@WilliamSnyders](https://github.com/WilliamSnyders) [Issue #19](https://github.com/rightscale/praxis/issues/19)
12
+ * Controller filters can now shortcut the request lifecycle flow by returning a `Response`:
13
+ * If a before filter returns it, both the action and the after filters will be skipped (as well as any remaining filters in the before list)
14
+ * If an after filter returns it, any remaining after filters in the block will be skipped.
15
+ * There is no way for the action result to skip the :after filters.
16
+ * Refactored Controller module to properly used ActiveSupprt concerns. [@jasonayre](https://github.com/jasonayre) [Issue #26](https://github.com/rightscale/praxis/issues/26)
17
+ * Separated the controller module into a Controller concern and a separable Callbacks concern
18
+ * Controller filters (i.e. callbacks) can shortcut request lifecycle by returning a Response object:
19
+ * If a before filter returns it, both the action and the after filters will be skipped (as well as any remaining before filters)
20
+ * If an after filter returns it, any remaining after filters in the block will be skipped.
21
+ * There is no way for the action result to skip the :after filters.
22
+ * Fixes [Issue #21](https://github.com/rightscale/praxis/issues/21)
23
+ * Introduced `around` filters using blocks:
24
+ * Around filters can be set wrapping any of the request stages (load, validate, action...) and might apply to only certain actions (i.e. exactly the same as the before/after filters)
25
+ * Therefore they supports the same attributes as `before` and `after` filters. The only difference is that an around filter block will get an extra parameter with the block to call to continue the chain.
26
+ * See the [Instances](https://github.com/rightscale/praxis/blob/master/spec/spec_app/app/controllers/instances.rb) controller for examples.
27
+ * Fix: Change :created response template to take an optiona ‘location’ parameter (instead of a media_type one, since it doesn’t make sense for a 201 type response) [Issue #26](https://github.com/rightscale/praxis/issues/23)
28
+ * Make the system be more robust in error reporting when controllers do not return a String or a Response
29
+ * Fix: ValidationError not setting a Content-Type header. [Issue #39](https://github.com/rightscale/praxis/issues/19)
30
+ * Relaxed ActiveSupport version dependency (from 4 to >=3 )
31
+ * Fix: InternalServerError not setting a Content-Type header. [Issue #42](https://github.com/rightscale/praxis/issues/42)
32
+ * A few document browser improvements:
33
+ * Avoid showing certain internal type options (i.e. reference).
34
+ * Fixed type label cide to detect collections better, and differentiate between Attributor ones and MediaType ones.
35
+ * Tweaked _example.html template to be much more collapsed by default, as it is not great, but makes it easier to review.
36
+ * Enhanced _links.html template to use the rs-type-label, and rs-attribute-description directives.
37
+ * Small design improvements on the home page for showing routes and verbs more prominently.
38
+ * Mediatype documentation improvements:
39
+ * Make `Links` always list its attributes when describe (never shallow)
40
+ * refactored MediaTypeCollection to store a member_attribute (instead of a member_type), and report it in describe much like attributor collections do.
41
+ * `MediaTypeCollection`. See [volume_snapshot](spec/spec_app/design/media_types/volume_snapshot.rb) in the specs for an example.
42
+ * Added `member_view` DSL to define a view that renders the collection's members with the given view.
43
+ * Change: Now requires all views to be explicitly defined (and will not automatically use the underlying member view if it exists). To define a view for member element (wrapping it in a collection) one can use the new member_view.
44
+ * Application.setup now returns the application instance so it can be chained to other application methods.
45
+ * This allows a more compact way to rackup the application: `run Praxis::Application.instance.setup`.
46
+ * Request headers are now backed by `Attributor::Hash` rather than (Structs)
47
+ ** Header keys are case sensitive now (although Praxis gracefully allows loading the uppercased RACK names )
48
+ * Added tests for using `Attributor::Hash` attributes that can accept "extra" keys:
49
+ ** See the `bulk_create` action for an example applied to `Praxis::Multipart` (which derives from `Attributor::Hash`) payload [here](https://github.com/rightscale/praxis/blob/master/spec/spec_app/design/resources/instances.rb).
50
+
51
+ ## 0.9 Initial release
data/CONTRIBUTING.md CHANGED
@@ -103,8 +103,9 @@ logical units of work using `git rebase -i` and `git push -f`. After every
103
103
  commit the test suite should be passing. Include documentation changes in the
104
104
  same commit so that a revert would remove all traces of the feature or fix.
105
105
 
106
- Commits that fix or close an issue should include a reference like `Closes
107
- #XXXX` or `Fixes #XXXX`, which will automatically close the issue when merged.
106
+ Commits that fix or close an issue should include a reference like
107
+ `Closes #XXXX` or `Fixes #XXXX`, which will automatically close the issue when
108
+ merged.
108
109
 
109
110
  Please do not add yourself to the `AUTHORS` file unless you are on the Praxis
110
111
  github team.
data/Gemfile CHANGED
@@ -1,3 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gemspec
3
+ gemspec
data/README.md CHANGED
@@ -48,12 +48,14 @@ job keeping the docs and the code in sync everytime they make code changes.
48
48
 
49
49
  ## Mailing List
50
50
  Join our Google Groups for discussion, support and announcements.
51
- * http://groups.google.com/d/forum/praxis-support (support for people using
51
+ * [praxis-support](http://groups.google.com/d/forum/praxis-support) (support for people using
52
52
  Praxis)
53
- * http://groups.google.com/d/forum/praxis-announce (announcements)
54
- * http://groups.google.com/d/forum/praxis-development (discussion about the
53
+ * [praxis-announce](http://groups.google.com/d/forum/praxis-announce) (announcements)
54
+ * [praxis-development](http://groups.google.com/d/forum/praxis-development) (discussion about the
55
55
  development of Praxis itself)
56
56
 
57
+ And follow us on twitter: [@praxisapi](http://twitter.com/praxisapi)
58
+
57
59
  ## Contributions
58
60
  Contributions to make Praxis better are welcome. Please refer to
59
61
  [CONTRIBUTING](https://github.com/rightscale/praxis/blob/master/CONTRIBUTING.md)
@@ -4376,6 +4376,10 @@ body {
4376
4376
  body a {
4377
4377
  cursor: pointer; }
4378
4378
 
4379
+ .content dl {
4380
+ margin-top: 0;
4381
+ margin-bottom: 0; }
4382
+
4379
4383
  .header .navbar-default {
4380
4384
  background-image: -ms-linear-gradient(top, #fff 0%, #f3f3f3 100%);
4381
4385
  background-image: -moz-linear-gradient(top, #fff 0%, #f3f3f3 100%);
@@ -10,7 +10,8 @@
10
10
 
11
11
  _.forEach(scope.attribute.options, function(option, name) {
12
12
  var templatePath = 'views/directives/attribute_description/_default.html';
13
-
13
+ var skip_keys = ['reference','dsl_compiler','dsl_compiler_options'];
14
+
14
15
  switch (name) {
15
16
  case 'example':
16
17
  // expects string
@@ -24,19 +25,21 @@
24
25
  break;
25
26
  }
26
27
 
27
- $http.get(templatePath, { cache: $templateCache }).success(function(template) {
28
- var row = $(template);
29
- var rowScope = scope.$new(true);
30
-
31
- rowScope.row = {
32
- name: name,
33
- value: option
34
- };
35
-
36
- $compile(row)(rowScope);
37
- list.append(row);
38
- });
39
-
28
+ if( ! _.contains( skip_keys, name ) ) {
29
+ $http.get(templatePath, { cache: $templateCache }).success(function(template) {
30
+ var row = $(template);
31
+ var rowScope = scope.$new(true);
32
+
33
+ rowScope.row = {
34
+ name: name,
35
+ value: option
36
+ };
37
+
38
+ $compile(row)(rowScope);
39
+ list.append(row);
40
+ });
41
+ }
42
+
40
43
  })
41
44
  }
42
45
  };
@@ -16,8 +16,8 @@
16
16
  var templates = {
17
17
  primitive: '<span>{{type.name}}</span>',
18
18
  type: '<a ui-sref="root.type({version: apiVersion, type: type.name})">{{type.name | resourceName}}</a>',
19
- typeCollection: '<span>Collection&nbsp;[&nbsp;{{type.options.member_attribute.type.name}}&nbsp;]</a>',
20
- primitiveCollection: '<span>Collection&nbsp;[&nbsp;<a ui-sref="root.type({version: apiVersion, type: type.options.member_attribute.type.name})">{{type.options.member_attribute.type.name | resourceName}}</a>&nbsp;]</span>',
19
+ primitiveCollection: '<span>Collection&nbsp;[&nbsp;{{type.member_attribute.type.name}}&nbsp;]</a>',
20
+ typeCollection: '<span>Collection&nbsp;[&nbsp;<a ui-sref="root.type({version: apiVersion, type: type.member_attribute.type.name})">{{type.member_attribute.type.name | resourceName}}</a>&nbsp;]</span>',
21
21
  link: '<span>Link&nbsp;[&nbsp;<a ui-sref="root.type({version: apiVersion, type: type.link_to})">{{type.link_to | resourceName}}</a>&nbsp;]</span>'
22
22
  };
23
23
 
@@ -29,15 +29,15 @@
29
29
  link: function(scope, element) {
30
30
 
31
31
  var template = templates.type;
32
-
33
32
  if (_.contains(primitives, scope.type.name)) {
34
33
  template = templates.primitive;
35
34
  }
36
- else if (scope.type.name === 'Collection') {
37
- if (_.contains(primitives, scope.type.options.member_attribute.type.name))
38
- template = templates.typeCollection;
39
- else
35
+ else if ( scope.type.member_attribute !== undefined ) {
36
+ if ( _.contains(primitives, scope.type.member_attribute.type.name)){
40
37
  template = templates.primitiveCollection;
38
+ }else{
39
+ template = templates.typeCollection;
40
+ }
41
41
  }
42
42
  else if (scope.type.link_to) {
43
43
  template = templates.link;
@@ -8,4 +8,11 @@ body {
8
8
  a {
9
9
  cursor: pointer;
10
10
  }
11
- }
11
+ }
12
+
13
+ .content {
14
+ dl {
15
+ margin-top: 0;
16
+ margin-bottom: 0;
17
+ }
18
+ }
@@ -1,8 +1,13 @@
1
- <dt ng-if="row.value.length < 250">example</dt>
2
- <dt ng-if="row.value.length >= 250">
3
- <a ng-init="row.isCollapsed = true" ng-click="row.isCollapsed = !row.isCollapsed">example (click to {{row.isCollapsed? 'expand' : 'collapse'}})</a>
1
+ <dt ng-if="row.value.length < 50">example</dt>
2
+ <dt ng-if="row.value.length >= 50">
3
+ example (<a ng-init="row.isCollapsed = true" ng-click="row.isCollapsed = !row.isCollapsed">{{row.isCollapsed? 'show' : 'hide'}}</a>)
4
+ </dt>
4
5
  </dt>
5
- <dd ng-if="row.value.length <= 50"><code>{{row.value}}</code></dd>
6
+
7
+
8
+ <dd ng-if="row.value.length <= 50">
9
+ <code ng-class="{hide: false}">{{row.value}}</code>
10
+ </dd>
6
11
  <dd ng-if="row.value.length > 50">
7
12
  <pre ng-class="{hide: row.isCollapsed}">{{row.value}}</pre>
8
13
  </dd>
@@ -1,10 +1,11 @@
1
1
  <ng-include src="'views/directives/attribute_table_row/_default.html'" no-container></ng-include>
2
- <div class="row" ng-repeat="(k,v) in attribute.options.dsl_compiler_options.links">
3
- <div class="col-sm-3" ng-bind-html="name + '.' + k | attributeName">
2
+ <div class="row" ng-repeat="(attribute_name,attribute) in attribute.type.attributes">
3
+ <div class="col-sm-3" ng-bind-html="name + '.' + attribute_name | attributeName">
4
4
  </div>
5
5
  <div class="col-sm-2">
6
+ <rs-type-label type="attribute.type"></rs-type-label>
6
7
  </div>
7
8
  <div class="col-sm-7">
8
- {{v}}
9
+ <rs-attribute-description attribute="attribute"></rs-attribute-description>
9
10
  </div>
10
11
  </div>
@@ -1,21 +1,27 @@
1
1
  <div class="attribute-table">
2
2
  <div class="row head">
3
- <div class="col-sm-3">
3
+ <div class="col-sm-2">
4
4
  Name
5
5
  </div>
6
- <div class="col-sm-9">
6
+ <div class="col-sm-4">
7
+ Route
8
+ </div>
9
+ <div class="col-sm-6">
7
10
  Description
8
11
  </div>
9
12
  </div>
10
13
  <div class="content">
11
14
  <div class="row" ng-repeat="action in controller.actions">
12
- <div class="col-sm-3">
15
+ <div class="col-sm-2">
13
16
  <a ui-sref="root.action({action: action.name, controller: controllerName, version: apiVersion})">{{ action.name }}</a>
14
17
  </div>
15
- <div class="col-sm-9">
18
+ <div class="col-sm-4">
16
19
  <div ng-repeat="url in action.urls">
17
- {{ url.verb }} {{ url.path }}
20
+ <div class="label label-default">{{ url.verb }}</div>
21
+ <strong>{{ url.path }}</strong>
18
22
  </div>
23
+ </div>
24
+ <div class="col-sm-6" style="font-size: 110%;">
19
25
  {{ action.description }}
20
26
  </div>
21
27
  </div>
@@ -2,7 +2,6 @@ module Praxis
2
2
  class ActionDefinition
3
3
  class HeadersDSLCompiler < Attributor::DSLCompiler
4
4
 
5
- alias_method :orig_attribute, :attribute
6
5
  # it allows to define expectations on incoming headers. For example:
7
6
  # header :X_SpecialCookie => implies the header is required
8
7
  # header :X_Something, /matching_this/ => implies that if the name header exists, it should match the regexp
@@ -22,13 +21,14 @@ module Praxis
22
21
  # Defining the existence without any other options can only mean that it is required (otherwise it is a useless definition)
23
22
  options[:required] = true if options.empty?
24
23
  end
25
- orig_attribute name.upcase , String, options
24
+ #orig_attribute name.upcase , String, options
25
+ key name , String, options
26
26
  end
27
27
 
28
- def attribute( name, type, **rest)
29
- raise Exceptions::InvalidConfiguration.new(
30
- "You cannot use the 'attribute' DSL inside a headers definition"
31
- )
28
+ # Override the attribute to really call "key" in the hash (for temporary backwards compat)
29
+ def attribute(name, attr_type=nil, **opts, &block)
30
+ warn "[DEPRECATION] `attribute` is deprecated when defining headers. Please use `key` instead."
31
+ key(name, attr_type, **opts, &block)
32
32
  end
33
33
 
34
34
  end
@@ -16,7 +16,6 @@ module Praxis
16
16
  attr_reader :named_routes
17
17
  attr_reader :responses
18
18
 
19
-
20
19
  def initialize(name, resource_definition, **opts, &block)
21
20
  @name = name
22
21
  @resource_definition = resource_definition
@@ -105,15 +104,31 @@ module Praxis
105
104
  end
106
105
  end
107
106
 
108
- def headers(**opts, &block)
107
+ def headers(type=Attributor::Hash.of(key:String), **opts, &block)
109
108
  return @headers unless block
110
-
111
109
  if @headers
112
110
  update_attribute(@headers, opts, block)
113
111
  else
114
- @headers = create_attribute(dsl_compiler: HeadersDSLCompiler, **opts, &block)
112
+ @headers = create_attribute(type,
113
+ dsl_compiler: HeadersDSLCompiler, case_insensitive_load: true,
114
+ **opts, &block)
115
+ end
116
+ @precomputed_header_keys_for_rack = nil #clear memoized data
117
+ end
118
+
119
+ # Good optimization to avoid creating lots of strings and comparisons
120
+ # on a per-request basis.
121
+ # However, this is hacky, as it is rack-specific, and does not really belong here
122
+ def precomputed_header_keys_for_rack
123
+ @precomputed_header_keys_for_rack ||= begin
124
+ @headers.attributes.keys.each_with_object(Hash.new) do |key,hash|
125
+ name = key.to_s
126
+ name = "HTTP_#{name.upcase}" unless ( name == "CONTENT_TYPE" || name == "CONTENT_LENGTH" )
127
+ hash[name] = key
128
+ end
115
129
  end
116
130
  end
131
+
117
132
 
118
133
  def routing(&block)
119
134
  routing_config = Skeletor::RestfulRoutingConfig.new(name, resource_definition, &block)
@@ -30,7 +30,7 @@ module Praxis
30
30
 
31
31
  def response(name)
32
32
  return @responses.fetch(name) do
33
- raise ArgumentError, "no response defined with name #{name.inspect}"
33
+ raise ArgumentError, "no response template defined with name #{name.inspect}. Are you forgetting to register it with ApiDefinition?"
34
34
  end
35
35
  end
36
36
 
@@ -48,8 +48,8 @@ module Praxis
48
48
  status 200
49
49
  end
50
50
 
51
- api.response_template :created do |media_type: |
52
- media_type media_type
51
+ api.response_template :created do |location: nil|
52
+ location location
53
53
  status 201
54
54
  end
55
55
  end
@@ -40,6 +40,7 @@ module Praxis
40
40
  @root = Pathname.new(root).expand_path
41
41
 
42
42
  @bootloader.setup!
43
+ self
43
44
  end
44
45
 
45
46
  def call(env)
@@ -7,7 +7,7 @@ module Praxis
7
7
 
8
8
  def execute
9
9
  application.controllers.each do |controller|
10
- controller.actions.each do |action_name, action|
10
+ controller.definition.actions.each do |action_name, action|
11
11
  action.routes.each do |route|
12
12
  target = target_factory(controller, action_name)
13
13
  application.router.add_route target, route
@@ -18,11 +18,11 @@ module Praxis
18
18
 
19
19
 
20
20
  def target_factory(controller, action_name)
21
- action = controller.action(action_name)
21
+ action = controller.definition.actions.fetch(action_name)
22
22
 
23
23
  Proc.new do |request|
24
24
  request.action = action
25
- dispatcher = Dispatcher.current(thread: Thread.current, application: application)
25
+ dispatcher = Dispatcher.current( application: application)
26
26
 
27
27
  dispatcher.dispatch(controller, action, request)
28
28
  end
@@ -17,6 +17,7 @@ module Praxis
17
17
  def execute
18
18
  return unless self.class.enabled
19
19
  base = application.file_layout[:app].base
20
+ return unless base.exist?
20
21
  file_enum = base.find.to_a
21
22
  files = file_enum.select do |file|
22
23
  path = file.relative_path_from(base)
@@ -0,0 +1,35 @@
1
+ require 'active_support/concern'
2
+
3
+ module Praxis
4
+ module Callbacks
5
+ extend ::ActiveSupport::Concern
6
+
7
+ included do
8
+ class_attribute :before_callbacks, :after_callbacks, :around_callbacks
9
+ self.before_callbacks = Hash.new
10
+ self.after_callbacks = Hash.new
11
+ self.around_callbacks = Hash.new
12
+ end
13
+
14
+ module ClassMethods
15
+
16
+ def before(*stage_path, **conditions, &block)
17
+ stage_path = [:action] if stage_path.empty?
18
+ before_callbacks[stage_path] ||= Array.new
19
+ before_callbacks[stage_path] << [conditions, block]
20
+ end
21
+
22
+ def after(*stage_path, **conditions, &block)
23
+ stage_path = [:action] if stage_path.empty?
24
+ after_callbacks[stage_path] ||= Array.new
25
+ after_callbacks[stage_path] << [conditions, block]
26
+ end
27
+
28
+ def around(*stage_path, **conditions, &block)
29
+ stage_path = [:action] if stage_path.empty?
30
+ around_callbacks[stage_path] ||= Array.new
31
+ around_callbacks[stage_path] << [conditions, block]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,59 +1,42 @@
1
1
  require 'active_support/concern'
2
- require 'active_support/inflector'
2
+ require 'active_support/all'
3
3
 
4
4
  module Praxis
5
-
6
5
  module Controller
7
- extend ActiveSupport::Concern
8
-
6
+ extend ::ActiveSupport::Concern
7
+ # A Controller always requires the callbacks
8
+ include Praxis::Callbacks
9
+
9
10
  included do
10
11
  attr_reader :request
11
12
  attr_accessor :response
12
- Application.instance.controllers << self
13
- self.instance_eval do
14
- @before_callbacks = Hash.new
15
- @after_callbacks = Hash.new
16
- end
17
-
18
13
  end
19
-
20
14
  module ClassMethods
21
- attr_reader :before_callbacks, :after_callbacks
22
-
23
15
  def implements(definition)
24
16
  define_singleton_method(:definition) do
25
17
  definition
26
18
  end
27
- definition.controller = self
28
- end
29
19
 
30
- def actions
31
- definition.actions
32
- end
33
-
34
- def action(name)
35
- actions.fetch(name)
36
- end
37
-
38
- def before(*stage_path, **conditions, &block)
39
- stage_path = [:action] if stage_path.empty?
40
- @before_callbacks[stage_path] ||= Array.new
41
- @before_callbacks[stage_path] << [conditions, block]
42
- end
43
-
44
- def after(*stage_path, **conditions, &block)
45
- stage_path = [:action] if stage_path.empty?
46
- @after_callbacks[stage_path] ||= Array.new
47
- @after_callbacks[stage_path] << [conditions, block]
20
+ definition.controller = self
21
+ Application.instance.controllers << self
48
22
  end
49
-
50
23
  end
51
24
 
52
-
53
25
  def initialize(request, response=Responses::Ok.new)
54
26
  @request = request
55
27
  @response = response
56
28
  end
57
29
 
30
+ # def request
31
+ # @request
32
+ # end
33
+ #
34
+ # def response
35
+ # @response
36
+ # end
37
+ #
38
+ # def response=(value)
39
+ # @response = value
40
+ # end
58
41
  end
59
42
  end
@@ -5,8 +5,14 @@ module Praxis
5
5
  attr_reader :groups, :base
6
6
 
7
7
  def initialize(base, &block)
8
+ if base.nil?
9
+ raise ArgumentError, "base must not be nil." \
10
+ "Are you missing a call Praxis::Application.instance.setup?"
11
+ end
12
+
13
+
8
14
  @groups = Hash.new
9
- @base = base
15
+ @base = Pathname.new(base)
10
16
 
11
17
  if block_given?
12
18
  self.instance_eval(&block)
@@ -18,16 +24,15 @@ module Praxis
18
24
  end
19
25
 
20
26
  def map(name, pattern, &block)
27
+ return unless base.exist?
28
+
21
29
  if block_given?
22
30
  @groups[name] = FileGroup.new(base + pattern, &block)
23
31
  else
24
32
  @groups[name] ||= []
25
- file_enum = base.find.to_a
26
- files = file_enum.select do |file|
27
- path = file.relative_path_from(base)
28
- file.file? && path.fnmatch?(pattern, File::FNM_PATHNAME)
29
- end
30
- files.sort_by { |file| [file.to_s.split('/').size, file.to_s] }.each { |file| @groups[name] << file }
33
+ files = Pathname.glob(base+pattern).select { |file| file.file? }
34
+ files.sort_by! { |file| [file.to_s.split('/').size, file.to_s] }
35
+ files.each { |file| @groups[name] << file }
31
36
  end
32
37
  end
33
38
 
data/lib/praxis/links.rb CHANGED
@@ -42,6 +42,10 @@ module Praxis
42
42
  self
43
43
  end
44
44
 
45
+ def self.describe(shallow=false)
46
+ super(false) # Links must always describe attributes
47
+ end
48
+
45
49
  def self._finalize!
46
50
  super
47
51
  if @attribute
@@ -23,6 +23,10 @@ module Praxis
23
23
  class MediaTypeCollection < MediaType
24
24
  include Enumerable
25
25
 
26
+ class << self
27
+ attr_accessor :member_attribute
28
+ end
29
+
26
30
  def self._finalize!
27
31
  super
28
32
 
@@ -35,10 +39,11 @@ module Praxis
35
39
  end
36
40
 
37
41
  def self.member_type(type=nil)
38
- return @media_type unless type
42
+ return ( @member_attribute ? @member_attribute.type : nil) unless type
39
43
  raise ArgumentError, "invalid type: #{type.name}" unless type < MediaType
40
44
 
41
- @member_type = type
45
+ member_options = {}
46
+ @member_attribute = Attributor::Attribute.new type, member_options
42
47
  end
43
48
 
44
49
  def self.example(context=nil, options: {})
@@ -59,10 +64,10 @@ module Praxis
59
64
 
60
65
  size.times do |i|
61
66
  subcontext = context + ["at(#{i})"]
62
- members << @member_type.example(subcontext)
67
+ members << @member_attribute.example(subcontext)
63
68
  end
64
69
 
65
-
70
+
66
71
  result.object._members = members
67
72
  result
68
73
  end
@@ -80,7 +85,7 @@ module Praxis
80
85
  self.new(self.attribute.load(value,context, **options))
81
86
  when Array
82
87
  object = self.attribute.load({})
83
- object._members = value.collect { |subvalue| @member_type.load(subvalue) }
88
+ object._members = value.collect { |subvalue| @member_attribute.load(subvalue) }
84
89
  self.new(object)
85
90
  else
86
91
  # Just wrap whatever value
@@ -88,18 +93,19 @@ module Praxis
88
93
  end
89
94
  end
90
95
 
96
+ def self.describe(shallow = false)
97
+ hash = super
98
+ hash[:member_attribute] = member_attribute.describe(true)
99
+ hash
100
+ end
91
101
 
92
- def render(view_name=:default, context: Attributor::DEFAULT_ROOT_CONTEXT)
93
- if (view = self.class.views[view_name])
94
- # we have the view ourselves, use it with our atrributes
95
- super
96
- else
97
- # render each member with the view
98
- @object.collect.with_index do |member, i|
99
- subcontext = context + ["at(#{i})"]
100
- member.render(view_name, context: subcontext)
101
- end
102
+ def self.member_view(name, using: nil)
103
+ if using
104
+ member_view = self.member_type.view(using)
105
+ return self.views[name] = CollectionView.new(name, self, member_view)
102
106
  end
107
+
108
+ self.views[name]
103
109
  end
104
110
 
105
111