praxis 0.18.1 → 0.19.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 (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