praxis 0.18.1 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/Gemfile +2 -1
  4. data/README.md +21 -27
  5. data/lib/api_browser/app/index.html +3 -3
  6. data/lib/api_browser/app/js/app.js +23 -3
  7. data/lib/api_browser/app/js/controllers/action.js +33 -21
  8. data/lib/api_browser/app/js/controllers/controller.js +3 -25
  9. data/lib/api_browser/app/js/controllers/menu.js +61 -51
  10. data/lib/api_browser/app/js/controllers/trait.js +10 -0
  11. data/lib/api_browser/app/js/controllers/type.js +8 -5
  12. data/lib/api_browser/app/js/directives/fixed_if_fits.js +9 -2
  13. data/lib/api_browser/app/js/directives/menu_item.js +59 -0
  14. data/lib/api_browser/app/js/directives/readable_list.js +87 -0
  15. data/lib/api_browser/app/js/directives/url.js +16 -0
  16. data/lib/api_browser/app/js/factories/Configuration.js +1 -2
  17. data/lib/api_browser/app/js/factories/Documentation.js +49 -7
  18. data/lib/api_browser/app/js/factories/PageInfo.js +9 -0
  19. data/lib/api_browser/app/js/factories/normalize_attributes.js +1 -2
  20. data/lib/api_browser/app/js/factories/template_for.js +9 -4
  21. data/lib/api_browser/app/sass/modules/_sidebar.scss +54 -15
  22. data/lib/api_browser/app/sass/praxis.scss +4 -0
  23. data/lib/api_browser/app/views/action.html +72 -41
  24. data/lib/api_browser/app/views/builtin/field-selector.html +24 -0
  25. data/lib/api_browser/app/views/controller.html +9 -10
  26. data/lib/api_browser/app/views/directives/menu_item.html +8 -0
  27. data/lib/api_browser/app/views/directives/url.html +3 -0
  28. data/lib/api_browser/app/views/layout.html +2 -2
  29. data/lib/api_browser/app/views/menu.html +8 -14
  30. data/lib/api_browser/app/views/navbar.html +1 -1
  31. data/lib/api_browser/app/views/trait.html +13 -0
  32. data/lib/api_browser/app/views/type/details.html +1 -1
  33. data/lib/api_browser/app/views/type.html +1 -1
  34. data/lib/api_browser/app/views/types/embedded/field-selector.html +13 -0
  35. data/lib/api_browser/app/views/types/label/primitive.html +1 -1
  36. data/lib/api_browser/app/views/types/standalone/array.html +3 -0
  37. data/lib/praxis/action_definition.rb +15 -2
  38. data/lib/praxis/collection.rb +17 -5
  39. data/lib/praxis/controller.rb +12 -3
  40. data/lib/praxis/docs/generator.rb +11 -7
  41. data/lib/praxis/extensions/field_expansion.rb +59 -0
  42. data/lib/praxis/extensions/field_selection/field_selector.rb +125 -0
  43. data/lib/praxis/extensions/field_selection.rb +10 -0
  44. data/lib/praxis/extensions/mapper_selectors.rb +16 -0
  45. data/lib/praxis/extensions/rendering.rb +43 -0
  46. data/lib/praxis/links.rb +1 -0
  47. data/lib/praxis/media_type.rb +87 -3
  48. data/lib/praxis/media_type_collection.rb +1 -1
  49. data/lib/praxis/media_type_identifier.rb +6 -1
  50. data/lib/praxis/plugins/praxis_mapper_plugin.rb +29 -10
  51. data/lib/praxis/restful_doc_generator.rb +11 -8
  52. data/lib/praxis/tasks/api_docs.rb +6 -5
  53. data/lib/praxis/types/multipart_array.rb +1 -1
  54. data/lib/praxis/version.rb +1 -1
  55. data/lib/praxis.rb +5 -0
  56. data/praxis.gemspec +4 -3
  57. data/spec/api_browser/factories/configuration_spec.js +32 -0
  58. data/spec/api_browser/factories/documentation_spec.js +75 -25
  59. data/spec/api_browser/factories/normalize_attributes_spec.js +0 -5
  60. data/spec/praxis/{types/collection_spec.rb → collection_spec.rb} +36 -23
  61. data/spec/praxis/extensions/field_expansion_spec.rb +96 -0
  62. data/spec/praxis/extensions/field_selection/field_selector_spec.rb +92 -0
  63. data/spec/praxis/extensions/rendering_spec.rb +63 -0
  64. data/spec/praxis/links_spec.rb +6 -0
  65. data/spec/praxis/media_type_collection_spec.rb +0 -1
  66. data/spec/praxis/media_type_identifier_spec.rb +15 -1
  67. data/spec/praxis/media_type_spec.rb +101 -3
  68. data/spec/praxis/plugins/praxis_mapper_plugin_spec.rb +33 -24
  69. data/spec/praxis/request_stages/request_stage_spec.rb +1 -1
  70. data/spec/praxis/types/multipart_array_spec.rb +14 -4
  71. data/spec/spec_app/app/controllers/instances.rb +6 -1
  72. data/spec/spec_app/config/environment.rb +2 -1
  73. data/spec/spec_app/design/resources/instances.rb +1 -0
  74. data/spec/spec_helper.rb +3 -1
  75. data/spec/support/spec_media_types.rb +224 -1
  76. metadata +50 -16
@@ -36,7 +36,7 @@ require 'terminal-table'
36
36
  # output into the Praxis::Application.instance.logger app log. Possible
37
37
  # values are: "detailed", "short", and "skip" (i.e. do not print the stats
38
38
  # at all).
39
- # 3. stats_log_level: the logging level with which the statistics should be logged.
39
+ # 3. stats_log_level: the logging level with which the statistics should be logged.
40
40
  #
41
41
  # See http://praxis-framework.io/reference/plugins/ for further details on how
42
42
  # to use a plugin and pass it options.
@@ -76,7 +76,7 @@ module Praxis
76
76
  def prepare_config!(node)
77
77
  node.attributes do
78
78
  attribute :log_stats, String, values: ['detailed', 'short', 'skip'], default: 'detailed'
79
- attribute :stats_log_level, Symbol, values: [:fatal,:error,:warn,:info,:debug], default: :info
79
+ attribute :stats_log_level, Symbol, values: [:fatal,:error,:warn,:info,:debug], default: :info
80
80
  attribute :repositories, Attributor::Hash.of(key: String, value: RepositoryConfig)
81
81
  end
82
82
  end
@@ -104,7 +104,8 @@ module Praxis
104
104
  log_stats = PraxisMapperPlugin::Plugin.instance.config.log_stats
105
105
  unless log_stats == 'skip'
106
106
  Praxis::Notifications.subscribe 'praxis.request.all' do |name, *junk, payload|
107
- if (identity_map = payload[:request].identity_map)
107
+ if (payload[:request].identity_map?)
108
+ identity_map = payload[:request].identity_map
108
109
  PraxisMapperPlugin::Statistics.log(payload[:request], identity_map, log_stats)
109
110
  end
110
111
  end
@@ -122,13 +123,17 @@ module Praxis
122
123
 
123
124
  module Request
124
125
  def identity_map
125
- @identity_map
126
+ @identity_map ||= Praxis::Mapper::IdentityMap.new
126
127
  end
127
128
 
128
129
  def identity_map=(map)
129
130
  @identity_map = map
130
131
  end
131
-
132
+
133
+ def identity_map?
134
+ !@identity_map.nil?
135
+ end
136
+
132
137
  def silence_mapper_stats
133
138
  @silence_mapper_stats
134
139
  end
@@ -143,10 +148,24 @@ module Praxis
143
148
  extend ActiveSupport::Concern
144
149
 
145
150
  included do
146
- before :action do |controller|
147
- controller.request.identity_map = Praxis::Mapper::IdentityMap.new
151
+ # Ensure we call #release on any identity map
152
+ # that may be set by the controller after the action
153
+ # completes.
154
+ around :action do |controller, callee|
155
+ begin
156
+ callee.call
157
+ ensure
158
+ if controller.request.identity_map?
159
+ controller.request.identity_map.release
160
+ end
161
+ end
148
162
  end
149
163
  end
164
+
165
+ def identity_map
166
+ request.identity_map
167
+ end
168
+
150
169
  end
151
170
 
152
171
  module Statistics
@@ -158,8 +177,8 @@ module Praxis
158
177
  self.to_logger "No database interactions observed."
159
178
  return
160
179
  end
161
-
162
-
180
+
181
+
163
182
  case log_stats
164
183
  when 'detailed'
165
184
  self.detailed(identity_map)
@@ -217,7 +236,7 @@ module Praxis
217
236
  def self.short(identity_map)
218
237
  self.to_logger identity_map.query_statistics.sum_totals.to_s
219
238
  end
220
-
239
+
221
240
  def self.to_logger(message)
222
241
  Praxis::Application.instance.logger.__send__(Plugin.instance.config.stats_log_level, "Praxis::Mapper Statistics: #{message}")
223
242
  end
@@ -17,7 +17,8 @@ module Praxis
17
17
  Attributor::Ids,
18
18
  Attributor::Integer,
19
19
  Attributor::Object,
20
- Attributor::String
20
+ Attributor::String,
21
+ Attributor::Symbol
21
22
  ]).freeze
22
23
 
23
24
  def self.inspect_attributes(the_type)
@@ -33,7 +34,7 @@ module Praxis
33
34
  if the_type.member_attribute.nil?
34
35
  the_type = the_type.member_type
35
36
  else
36
- the_type = the_type.member_attribute.type
37
+ the_type = the_type.member_attribute.type
37
38
  end
38
39
  end
39
40
 
@@ -74,9 +75,6 @@ module Praxis
74
75
  # Collect reachable types from the media_type if any (plus itself)
75
76
  if @media_type && ! @media_type.is_a?(Praxis::SimpleMediaType)
76
77
  add_to_reachable RestfulDocGenerator.inspect_attributes(@media_type)
77
- @media_type.attributes.each do |name, attr|
78
- add_to_reachable RestfulDocGenerator.inspect_attributes(attr)
79
- end
80
78
  @generated_example = @media_type.example(self.id)
81
79
  end
82
80
 
@@ -133,6 +131,10 @@ module Praxis
133
131
  return if found == nil
134
132
  @reachable_types += found
135
133
  end
134
+
135
+ def display_name
136
+ @controller_config.display_name
137
+ end
136
138
  end
137
139
 
138
140
  def initialize(root_dir)
@@ -141,7 +143,7 @@ module Praxis
141
143
  @resources = []
142
144
 
143
145
  Attributor::AttributeResolver.current = Attributor::AttributeResolver.new
144
-
146
+
145
147
  remove_previous_doc_data
146
148
  load_resources
147
149
 
@@ -276,7 +278,7 @@ module Praxis
276
278
  info[:media_type] = r.media_type.id
277
279
  media_types_seen_from_controllers << r.media_type
278
280
  end
279
- display_name = r.name.split("::").last
281
+ display_name = r.display_name #.name.split("::").last
280
282
  index[r.version][display_name] = info
281
283
  end
282
284
 
@@ -288,7 +290,8 @@ module Praxis
288
290
 
289
291
  reportable_types.each do |type|
290
292
  index[version] ||= Hash.new
291
- display_name = type.name.split("::").last
293
+ next unless type.respond_to?(:display_name)
294
+ display_name = type.display_name #.name.split("::").last
292
295
  if index[version].has_key? display_name
293
296
  raise "Display name already taken for version #{version}! #{display_name}"
294
297
  end
@@ -37,16 +37,17 @@ namespace :praxis do
37
37
  exec({'USER_DOCS_PATH' => File.join(Dir.pwd, 'docs')}, "#{path}/node_modules/.bin/grunt build --gruntfile '#{path}/Gruntfile.js'")
38
38
  end
39
39
 
40
- desc "Generate API docs (JSON definitions) for a Praxis App"
41
- task :generate => [:environment] do |t, args|
40
+ desc "Generate deprecated API docs (JSON definitions) for a Praxis App"
41
+ task :generate_old => [:environment] do |t, args|
42
42
  require 'fileutils'
43
+ STDERR.puts "DEPRECATION: praxis:docs:generate_old is deprecated and will be removed in the next version. Please update tooling that may need this."
43
44
 
44
45
  Praxis::Blueprint.caching_enabled = false
45
46
  generator = Praxis::RestfulDocGenerator.new(Dir.pwd)
46
47
  end
47
48
 
48
- desc "Generate BETA API docs (JSON definitions) for a Praxis App"
49
- task :generate_beta => [:environment] do |t, args|
49
+ desc "Generate API docs (JSON definitions) for a Praxis App"
50
+ task :generate => [:environment] do |t, args|
50
51
  require 'fileutils'
51
52
 
52
53
  Praxis::Blueprint.caching_enabled = false
@@ -59,7 +60,7 @@ namespace :praxis do
59
60
  desc "Generate API docs (JSON definitions) for a Praxis App"
60
61
  task :api_docs do
61
62
  STDERR.puts "DEPRECATION: praxis:api_docs is deprecated and will be removed by 1.0. Please use praxis:docs:generate instead."
62
- Rake::Task["praxis:docs:generate"].invoke
63
+ Rake::Task["praxis:docs:generate_old"].invoke
63
64
  end
64
65
 
65
66
  desc "Run API Documentation Browser"
@@ -110,7 +110,7 @@ module Praxis
110
110
  end
111
111
 
112
112
  def self.file(name, payload_type=nil, filename: nil, **opts, &block)
113
- self.part(name, payload_type=nil, filename: true, **opts, &block)
113
+ self.part(name, payload_type, filename: true, **opts, &block)
114
114
  end
115
115
 
116
116
  def self.load(value, context=Attributor::DEFAULT_ROOT_CONTEXT, content_type:nil)
@@ -1,3 +1,3 @@
1
1
  module Praxis
2
- VERSION = '0.18.1'
2
+ VERSION = '0.19.0'
3
3
  end
data/lib/praxis.rb CHANGED
@@ -83,6 +83,11 @@ module Praxis
83
83
  autoload :Validation, 'praxis/exceptions/validation'
84
84
  end
85
85
 
86
+ module Extensions
87
+ autoload :MapperSelectors, 'praxis/extensions/mapper_selectors'
88
+ autoload :Rendering, 'praxis/extensions/rendering'
89
+ autoload :FieldExpansion, 'praxis/extensions/field_expansion'
90
+ end
86
91
 
87
92
  module Handlers
88
93
  autoload :Plain, 'praxis/handlers/plain'
data/praxis.gemspec CHANGED
@@ -24,9 +24,9 @@ Gem::Specification.new do |spec|
24
24
  spec.add_dependency 'mustermann', '~> 0'
25
25
  spec.add_dependency 'activesupport', '>= 3'
26
26
  spec.add_dependency 'mime', '~> 0'
27
- spec.add_dependency 'praxis-mapper', '>= 4.1'
28
- spec.add_dependency 'praxis-blueprints', '>= 2.2'
29
- spec.add_dependency 'attributor', '>= 4.0.1'
27
+ spec.add_dependency 'praxis-mapper', '~> 4.2'
28
+ spec.add_dependency 'praxis-blueprints', '~> 3.1'
29
+ spec.add_dependency 'attributor', '~> 5.0'
30
30
  spec.add_dependency 'thor', '~> 0.18'
31
31
  spec.add_dependency 'terminal-table', '~> 1.4'
32
32
  spec.add_dependency 'harness', '~> 2'
@@ -48,4 +48,5 @@ Gem::Specification.new do |spec|
48
48
  spec.add_development_dependency 'fuubar', '~> 2'
49
49
  spec.add_development_dependency 'yard', '~> 0'
50
50
  spec.add_development_dependency 'sqlite3', '~> 1'
51
+ spec.add_development_dependency 'coveralls'
51
52
  end
@@ -0,0 +1,32 @@
1
+ describe('Configuration', function() {
2
+ var theConfigProvider, runFn;
3
+ beforeEach(function() {
4
+ angular.module('PraxisDocBrowser').config(function(ConfigurationProvider) {
5
+ theConfigProvider = ConfigurationProvider;
6
+ });
7
+ runFn = angular.module('PraxisDocBrowser')._runBlocks[0];
8
+ });
9
+
10
+ beforeEach(angular.mock.module('PraxisDocBrowser'));
11
+
12
+ beforeEach(inject(function(Configuration) {
13
+ Configuration;
14
+ }));
15
+
16
+ it('sets sensible defaults', function() {
17
+ expect(theConfigProvider).toEqual(jasmine.objectContaining({
18
+ title: 'API Browser',
19
+ versionLabel: 'API Version',
20
+ expandChildren: true
21
+ }));
22
+ });
23
+
24
+ it('assigns all things from the provider unto the root scope', inject(function($injector, $rootScope) {
25
+ $injector.invoke(runFn);
26
+ expect($rootScope).toEqual(jasmine.objectContaining({
27
+ title: 'API Browser',
28
+ versionLabel: 'API Version',
29
+ expandChildren: true
30
+ }));
31
+ }));
32
+ });
@@ -4,9 +4,33 @@ describe('Documentation service', function() {
4
4
  beforeEach(angular.mock.module('PraxisDocBrowser'));
5
5
 
6
6
  beforeEach(inject(function($rootScope, $injector) {
7
+ $httpBackend = $injector.get('$httpBackend');
8
+ $httpBackend.expectGET('api/index-new.json').respond({
9
+ versions: ['1.0', '2.0']
10
+ });
11
+ $httpBackend.expectGET('api/1.0.json').respond({
12
+ resources: {
13
+ foo: {
14
+
15
+ }
16
+ },
17
+ schemas: {
18
+ bar: {
19
+
20
+ }
21
+ }
22
+ });
23
+ $httpBackend.expectGET('api/2.0.json').respond({
24
+ resources: {
25
+ test: 'TestCTRL'
26
+ },
27
+ schemas: {
28
+ testType: 'TestType'
29
+ }
30
+ });
7
31
  $scope = $rootScope.$new();
8
32
  Documentation = $injector.get('Documentation');
9
- $httpBackend = $injector.get('$httpBackend');
33
+ $httpBackend.flush();
10
34
  }));
11
35
 
12
36
  afterEach(function() {
@@ -14,37 +38,63 @@ describe('Documentation service', function() {
14
38
  $httpBackend.verifyNoOutstandingRequest();
15
39
  });
16
40
 
17
- describe('#getIndex', function() {
18
- var result, response = {
19
- '1.0': {
20
- 'Blogs': {
21
- 'controller': 'V1-Controllers-Blogs',
22
- 'name': 'V1::Controllers::Blogs',
23
- 'media_type': 'V1-MediaTypes-Blog'
24
- },
25
- 'Posts': {
26
- 'controller': 'V1-ResourceDefinitions-Posts',
27
- 'name': 'V1::ResourceDefinitions::Posts',
28
- 'media_type': 'V1-MediaTypes-Post'
41
+ describe('#versions', function() {
42
+ var versions;
43
+ beforeEach(function() {
44
+ Documentation.versions().then(function(v) {
45
+ versions = v;
46
+ });
47
+ $scope.$apply();
48
+ });
49
+ it('returns both versions', function() {
50
+ expect(versions).toEqual(['1.0', '2.0']);
51
+ });
52
+ });
53
+
54
+ describe('#items', function() {
55
+ var items;
56
+ beforeEach(function() {
57
+ Documentation.items('1.0').then(function(data) {
58
+ items = data;
59
+ });
60
+ $scope.$apply();
61
+ });
62
+
63
+ it('returns all items for 1.0', function() {
64
+ expect(items).toEqual({
65
+ resources: {
66
+ foo: { }
29
67
  },
30
- 'Users': {
31
- 'controller': 'V1-ResourceDefinitions-Users',
32
- 'name': 'V1::ResourceDefinitions::Users',
33
- 'media_type': 'V1-MediaTypes-User'
68
+ schemas: {
69
+ bar: {}
34
70
  }
35
- }
36
- };
71
+ });
72
+ });
73
+ });
74
+ describe('#controller', function() {
75
+ var item;
76
+ beforeEach(function() {
77
+ Documentation.controller('2.0', 'test').then(function(data) {
78
+ item = data;
79
+ });
80
+ $scope.$apply();
81
+ });
82
+
83
+ it('returns all items for 1.0', function() {
84
+ expect(item).toEqual('TestCTRL');
85
+ });
86
+ });
87
+ describe('#type', function() {
88
+ var item;
37
89
  beforeEach(function() {
38
- $httpBackend.expectGET('api/index.json').respond(response);
39
- Documentation.getIndex().then(function(data) {
40
- result = data;
90
+ Documentation.type('2.0', 'testType').then(function(data) {
91
+ item = data;
41
92
  });
42
- $httpBackend.flush();
43
93
  $scope.$apply();
44
94
  });
45
95
 
46
- it('returns the index data', function() {
47
- expect(result.data).toEqual(response);
96
+ it('returns all items for 1.0', function() {
97
+ expect(item).toEqual('TestType');
48
98
  });
49
99
  });
50
100
  });
@@ -44,30 +44,25 @@ describe('normalizeAttributes', function() {
44
44
  test1: {
45
45
  options: {
46
46
  values: ['Hello'],
47
- example: '"Hello"'
48
47
  }
49
48
  },
50
49
  test2: {
51
50
  options: {
52
- example: '{\n "test3": "Yeah",\n "moreRecursive": {\n "test4": 3\n }\n}'
53
51
  },
54
52
  type: {
55
53
  attributes: {
56
54
  test3: {
57
55
  options: {
58
56
  default: 'Yeah',
59
- example: '"Yeah"'
60
57
  }
61
58
  },
62
59
  moreRecursive: {
63
60
  options: {
64
- example: '{\n "test4": 3\n}'
65
61
  },
66
62
  type: {
67
63
  attributes: {
68
64
  test4: {
69
65
  options: {
70
- example: '3'
71
66
  }
72
67
  }
73
68
  }
@@ -2,34 +2,28 @@ require "spec_helper"
2
2
 
3
3
  describe Praxis::Collection do
4
4
 
5
- let(:type) { Volume }
6
- let(:example) { Volume.example('example-volume') }
5
+ let(:member_type) { Volume }
7
6
 
8
- let(:snapshots) { example.snapshots }
9
7
 
10
- subject(:media_type_collection) do
11
- Volume.attributes[:snapshots].type
8
+ subject!(:collection) do
9
+ Praxis::Collection.of(member_type)
12
10
  end
13
11
 
14
12
  context '.of' do
15
- let(:media_type) do
13
+ let(:member_type) do
16
14
  Class.new(Praxis::MediaType) do
17
15
  identifier 'application/an-awesome-type'
18
16
  end
19
17
  end
20
18
 
21
- subject!(:collection) do
22
- Praxis::Collection.of(media_type)
23
- end
24
-
25
19
  its(:identifier) { should eq 'application/an-awesome-type; type=collection' }
26
20
 
27
21
  it 'sets the collection on the media type' do
28
- expect(media_type::Collection).to be(collection)
22
+ expect(member_type::Collection).to be(collection)
29
23
  end
30
24
 
31
25
  it 'returns an existing Collection type' do
32
- expect(Praxis::Collection.of(media_type)).to be(collection)
26
+ expect(Praxis::Collection.of(member_type)).to be(collection)
33
27
  end
34
28
 
35
29
  it 'works with explicitly-defined collections' do
@@ -41,15 +35,34 @@ describe Praxis::Collection do
41
35
  subject(:type) { Volume::Collection }
42
36
  its(:member_type) { should be Volume }
43
37
  its(:identifier) { should eq 'application/vnd.acme.volumes' }
44
-
45
38
  end
46
39
 
47
40
  context '.member_type' do
48
- its(:member_type){ should be(VolumeSnapshot) }
41
+ subject(:collection) do
42
+ class Team < Praxis::Collection
43
+ member_type Person
44
+ end
45
+ Team
46
+ end
47
+ its(:member_type){ should be(Person) }
49
48
  its(:member_attribute){ should be_kind_of(Attributor::Attribute) }
50
- its('member_attribute.type'){ should be(VolumeSnapshot) }
49
+ its('member_attribute.type'){ should be(Person) }
50
+ its(:identifier) { should eq Person.identifier + "; type=collection" }
51
51
  end
52
-
52
+
53
+ context '.views' do
54
+ subject(:views) { collection.views }
55
+ its(:keys) { should match_array(member_type.views.keys)}
56
+
57
+ it 'generates CollectionViews from the member views' do
58
+ collection.views.each do |name, view|
59
+ expect(view.name).to be name
60
+ expect(view.schema).to be member_type
61
+ expect(view.contents).to eq member_type.views[name].contents
62
+ end
63
+ end
64
+ end
65
+
53
66
  context '.load' do
54
67
  let(:volume_data) do
55
68
  {
@@ -86,19 +99,19 @@ describe Praxis::Collection do
86
99
  end
87
100
 
88
101
 
89
- context '#render' do
90
-
91
102
 
103
+ context '#render' do
92
104
  context 'for members' do
105
+ let(:example) { Volume.example('example-volume') }
106
+ let(:snapshots) { example.snapshots }
107
+
93
108
  let(:volume_output) { example.render(view: :default) }
94
109
 
95
- subject(:output) { volume_output[:snapshots] }
110
+ subject(:output) { volume_output[:snapshots] }
96
111
 
97
112
  it { should eq(snapshots.collect(&:render)) }
98
113
  end
99
-
100
-
101
- end
114
+ end
102
115
 
103
116
  context '#validate' do
104
117
  let(:volume_data) do
@@ -122,7 +135,7 @@ describe Praxis::Collection do
122
135
 
123
136
  let(:volume) { Volume.load(volume_data) }
124
137
  let(:snapshots) { volume.snapshots }
125
-
138
+
126
139
  it 'validates' do
127
140
  expect(volume.validate).to be_empty
128
141
  end
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+
3
+ require 'praxis/extensions/field_selection'
4
+
5
+ describe Praxis::Extensions::FieldExpansion do
6
+
7
+ # include the ActionDefinitionExtension module directly, as that's where the
8
+ # bulk of lies, and by including this instead of the base FieldExpansion module
9
+ # we avoid the side-effect of injecting the ActionDefinitionExtension into
10
+ # the base Praxis::ActionDefinition.
11
+ let(:test_class) do
12
+ Class.new do
13
+ include Praxis::Extensions::FieldExpansion::ActionDefinitionExtension
14
+
15
+ def initialize(params)
16
+ @params = params
17
+ end
18
+
19
+ attr_accessor :params
20
+ end
21
+ end
22
+
23
+ let(:test_instance) { test_class.new(test_params) }
24
+
25
+ let(:request_params) do
26
+ double('params',
27
+ fields: Praxis::Extensions::FieldSelection::FieldSelector.for(Person).load(fields),
28
+ view: view
29
+ )
30
+ end
31
+
32
+ let(:request) { double('Praxis::Request', params: request_params) }
33
+ let(:media_type) { Person }
34
+
35
+ let(:fields) { nil }
36
+ let(:view) { nil }
37
+
38
+ let(:test_attributes) { }
39
+ let(:test_params) { double('test_params', attributes: test_attributes) }
40
+
41
+ subject(:expansion) { test_instance.expanded_fields(request, media_type)}
42
+
43
+ context '#expanded_fields' do
44
+
45
+ context 'with fields and view params defined' do
46
+ let(:test_attributes) { {view: true, fields: true} }
47
+
48
+ context 'and no fields provided' do
49
+ it 'returns the fields for the default view' do
50
+ expect(expansion).to eq({id: true, name: true, links: [true]})
51
+ end
52
+
53
+ context 'and a view' do
54
+ let(:view) { :link }
55
+ it 'expands the fields on the view' do
56
+ expect(expansion).to eq({id: true, name: true, href: true})
57
+ end
58
+ end
59
+ end
60
+
61
+ context 'with a set of fields provided' do
62
+ let(:fields) { 'id,name,owner{name}' }
63
+ it 'returns the subset of fields for the default view' do
64
+ expected = {id: true, name: true }
65
+ expect(expansion).to eq expected
66
+ end
67
+
68
+ context 'and a view' do
69
+ let(:view) { :link }
70
+ let(:fields) { 'id,href' }
71
+
72
+ it 'returns the subset of fields that exist for the view' do
73
+ expected = {id: true, href: true }
74
+ expect(expansion).to eq expected
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ context 'with only a view param defined' do
81
+ let(:test_attributes) { {view: true} }
82
+
83
+ it 'returns the fields for the default view' do
84
+ expect(expansion).to eq({id: true, name: true, links: [true]})
85
+ end
86
+
87
+ context 'and a view' do
88
+ let(:view) { :link }
89
+ it 'expands the fields on the view' do
90
+ expect(expansion).to eq({id: true, name: true, href: true})
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ end