praxis 0.9 → 0.10.0

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 (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