pragma 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 474ef14673089ce28a581b260a0c8913b3e1afe462eeb7cb840c406a29195316
4
- data.tar.gz: 15174e9df88e96007f15475464d023eab01c92e3b791f8c0289382f65a01c279
3
+ metadata.gz: bd39da8a90334eb2afe3aeffae4bf1b83b1f2847f7a501065bcea8f0df15e9d0
4
+ data.tar.gz: e4bd66a3c01463012edc6502eb636473338eeaae434d1b1544bd7d5166e40f08
5
5
  SHA512:
6
- metadata.gz: b1a710dd54c3a2aff7fa789293d160e0061c61e9d3fd86d3b00c8360efa6171de79719803c89a352ac6059895d3e1b4fdfe5d6c54e5e73e6ea0cbcdf68e0bc8c
7
- data.tar.gz: 62440fa3450e7e40314e92d6a771c072657130cfd785b05cabf2becf736fb2429be7f012882308572791c2e6e113f52a206f199b5b549cc7385ac91d8d79748d
6
+ metadata.gz: 36d424c34488a97c82b80974e558e7cd317ca9c2b1f1eb8f5961ea1b07e3b8a9d33696e8f2aca48059315fb17b6bff3dd5ee5f3eea30f802328f00f5f76f7096
7
+ data.tar.gz: 7f9832ae44752caf106f08ea0acdf0a5e24b1aa0b10850987e5461f2c4dc83c027f4ece5e3c365dc10fe1743d0b105b5424141ca87c09b363acbd4b7b05934e2
@@ -33,16 +33,16 @@ Metrics/LineLength:
33
33
  Max: 100
34
34
  AllowURI: true
35
35
 
36
- Layout/FirstParameterIndentation:
36
+ Layout/IndentFirstArgument:
37
37
  Enabled: false
38
38
 
39
39
  Layout/MultilineMethodCallIndentation:
40
40
  EnforcedStyle: indented
41
41
 
42
- Layout/IndentArray:
42
+ Layout/IndentFirstArrayElement:
43
43
  EnforcedStyle: consistent
44
44
 
45
- Layout/IndentHash:
45
+ Layout/IndentFirstHashElement:
46
46
  EnforcedStyle: consistent
47
47
 
48
48
  Style/SignalException:
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.5.0]
11
+
12
+ ### Added
13
+
14
+ - Expose `type` property for API errors
15
+ - Expanded associations are now eager-loaded automatically
16
+ - Calling macros whose support classes are undefined will now raise a `MissingSkillError`
17
+
18
+ ### Fixed
19
+
20
+ - Fixed computation of classes for nested operations
21
+
10
22
  ## [2.4.0]
11
23
 
12
24
  ### Added
@@ -76,7 +88,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
76
88
 
77
89
  First Pragma 2 release.
78
90
 
79
- [Unreleased]: https://github.com/pragmarb/pragma/compare/v2.4.0...HEAD
91
+ [Unreleased]: https://github.com/pragmarb/pragma/compare/v2.5.0...HEAD
92
+ [2.5.0]: https://github.com/pragmarb/pragma/compare/v2.4.0...v2.5.0
80
93
  [2.4.0]: https://github.com/pragmarb/pragma/compare/v2.3.0...v2.4.0
81
94
  [2.3.0]: https://github.com/pragmarb/pragma/compare/v2.2.0...v2.3.0
82
95
  [2.2.0]: https://github.com/pragmarb/pragma/compare/v2.1.1...v2.2.0
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'adaptor'
4
+
3
5
  require 'pragma/operation'
4
6
  require 'pragma/policy'
5
7
  require 'pragma/contract'
@@ -19,6 +21,7 @@ require 'pragma/filter/boolean'
19
21
 
20
22
  require 'pragma/operation/filter'
21
23
 
24
+ require 'pragma/macro'
22
25
  require 'pragma/macro/classes'
23
26
  require 'pragma/macro/decorator'
24
27
  require 'pragma/macro/filtering'
@@ -32,6 +35,11 @@ require 'pragma/macro/contract/persist'
32
35
 
33
36
  require 'pragma/operation/macro'
34
37
 
38
+ require 'pragma/association_includer/base'
39
+ require 'pragma/association_includer/active_record'
40
+ require 'pragma/association_includer/poro'
41
+ require 'pragma/association_includer'
42
+
35
43
  require 'pragma/operation/index'
36
44
  require 'pragma/operation/show'
37
45
  require 'pragma/operation/create'
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module AssociationIncluder
5
+ include Adaptor::Loader
6
+ register ActiveRecord, Poro
7
+ end
8
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module AssociationIncluder
5
+ class ActiveRecord < Base
6
+ class << self
7
+ def supports?(relation)
8
+ defined?(::ActiveRecord::Relation) && relation.is_a?(::ActiveRecord::Relation)
9
+ end
10
+ end
11
+
12
+ def include_associations(expands)
13
+ relation.includes(validate_associations(
14
+ relation.model,
15
+ destruct_associations(expands)
16
+ ))
17
+ end
18
+
19
+ private
20
+
21
+ def destruct_associations(expands)
22
+ associations = {}
23
+
24
+ expands.each do |expand|
25
+ expand.split('.').inject(associations) do |accumulator, association|
26
+ accumulator[association] ||= {}
27
+ end
28
+ end
29
+
30
+ associations
31
+ end
32
+
33
+ def validate_associations(model, associations)
34
+ Hash[associations.map do |(key, value)|
35
+ reflection = model.reflect_on_association(key.to_sym)
36
+ reflection ? [key, validate_associations(reflection.klass, value)] : [false, false]
37
+ end.select { |_, v| v }]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module AssociationIncluder
5
+ class Base
6
+ attr_reader :relation
7
+
8
+ class << self
9
+ def supports?(_relation)
10
+ fail NotImplementedError
11
+ end
12
+ end
13
+
14
+ def initialize(relation)
15
+ @relation = relation
16
+ end
17
+
18
+ def include_associations(_expands)
19
+ fail NotImplementedError
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module AssociationIncluder
5
+ class Poro < Base
6
+ class << self
7
+ def supports?(_relation)
8
+ true
9
+ end
10
+ end
11
+
12
+ def include_associations(_expands)
13
+ relation
14
+ end
15
+ end
16
+ end
17
+ end
@@ -3,9 +3,15 @@
3
3
  module Pragma
4
4
  module Decorator
5
5
  class Error < Pragma::Decorator::Base
6
+ include Pragma::Decorator::Type
7
+
6
8
  property :error_type
7
9
  property :error_message
8
10
  property :meta
11
+
12
+ def type
13
+ :error
14
+ end
9
15
  end
10
16
  end
11
17
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pragma
4
+ module Macro
5
+ class << self
6
+ # Returns a skill or raises a {#MissingSkillError}.
7
+ #
8
+ # @param macro [String] the name of the macro requiring the skill
9
+ # @param skill [String] the name of the string
10
+ # @param options [Hash] the +options+ hash of the operation
11
+ #
12
+ # @return [Object] the value of the skill
13
+ #
14
+ # @raise [MissingSkillError] if the skill is undefined or +nil+
15
+ #
16
+ # @private
17
+ def require_skill(macro, skill, options)
18
+ options[skill] || fail(MissingSkillError.new(macro, skill))
19
+ end
20
+ end
21
+
22
+ # Error raised when a skill is required but not present.
23
+ #
24
+ # @private
25
+ class MissingSkillError < StandardError
26
+ # Initializes the error.
27
+ #
28
+ # @param macro [String] the macro requiring the skill
29
+ # @param skill [String] the name of the missing skill
30
+ def initialize(macro, skill)
31
+ message = <<~ERROR
32
+ You are attempting to use the #{macro} macro, but no `#{skill}' skill is defined.
33
+
34
+ You can define the skill by adding the following to your operation:
35
+
36
+ self['#{skill}'] = MyCustomClass
37
+
38
+ If the skill holds a class, this can happen when the required class (e.g. the contract
39
+ class) is not in the expected location. If that's the case, you can just move the class to
40
+ the expected location and avoid defining the skill manually.
41
+ ERROR
42
+
43
+ super message
44
+ end
45
+ end
46
+ end
47
+ end
@@ -42,13 +42,15 @@ module Pragma
42
42
  private
43
43
 
44
44
  def resource_namespace(input, _options)
45
- input.class.name.split('::')[0..-3]
45
+ parts = input.class.name.split('::')
46
+ parts[0..(parts.index('Operation') - 1)]
46
47
  end
47
48
 
48
49
  def root_namespace(input, options)
49
50
  resource_namespace = resource_namespace(input, options)
50
- return [] if %w[API Api].include?(resource_namespace.first)
51
- api_index = (resource_namespace.index('API') || resource_namespace.index('Api') || 1)
51
+ return [] if resource_namespace.first.casecmp('API').zero?
52
+
53
+ api_index = (resource_namespace.map(&:upcase).index('API') || 1)
52
54
  resource_namespace[0..(api_index - 1)]
53
55
  end
54
56
 
@@ -87,10 +89,12 @@ module Pragma
87
89
  end
88
90
 
89
91
  def expected_contract_class(input, options)
92
+ parts = input.class.name.split('::')
93
+
90
94
  [
91
95
  resource_namespace(input, options),
92
96
  'Contract',
93
- input.class.name.split('::').last
97
+ parts[(parts.index('Operation') + 1)..-1]
94
98
  ].join('::')
95
99
  end
96
100
  end
@@ -7,6 +7,8 @@ module Pragma
7
7
  module Contract
8
8
  def self.Build(name: 'default', constant: nil, builder: nil)
9
9
  step = lambda do |input, options|
10
+ Macro.require_skill('Contract::Build', "contract.#{name}.class", options)
11
+
10
12
  Trailblazer::Operation::Contract::Build.for(
11
13
  input,
12
14
  options,
@@ -7,6 +7,8 @@ module Pragma
7
7
  module Contract
8
8
  def self.Persist(method: :save, name: 'default')
9
9
  step = lambda do |input, options|
10
+ Macro.require_skill('Contract::Persist', "contract.#{name}.class", options)
11
+
10
12
  Trailblazer::Operation::Pipetree::Step.new(
11
13
  Trailblazer::Operation::Contract::Persist(method: method, name: name).first
12
14
  ).call(input, options).tap do |result|
@@ -7,6 +7,8 @@ module Pragma
7
7
  module Contract
8
8
  def self.Validate(name: 'default', **args)
9
9
  step = lambda do |input, options|
10
+ Macro.require_skill('Contract::Validate', "contract.#{name}.class", options)
11
+
10
12
  Trailblazer::Operation::Pipetree::Step.new(
11
13
  Trailblazer::Operation::Contract::Validate(**args).first
12
14
  ).call(input, options).tap do |result|
@@ -10,13 +10,13 @@ module Pragma
10
10
  module Decorator
11
11
  class << self
12
12
  def for(_input, name, options)
13
+ klass = Macro.require_skill('Decorator', "decorator.#{name}.class", options)
14
+
13
15
  set_defaults(options)
14
16
 
15
17
  return false unless validate_params(options)
16
18
 
17
- options["result.decorator.#{name}"] = options["decorator.#{name}.class"].new(
18
- options['model']
19
- )
19
+ options["result.decorator.#{name}"] = klass.new(options['model'])
20
20
 
21
21
  validate_expansion(options, name)
22
22
  end
@@ -63,6 +63,7 @@ module Pragma
63
63
 
64
64
  def validate_expansion(options, name)
65
65
  return true unless options["result.decorator.#{name}"].respond_to?(:validate_expansion)
66
+
66
67
  options["result.decorator.#{name}"].validate_expansion(options['params'][:expand])
67
68
  true
68
69
  rescue Pragma::Decorator::Association::ExpansionError => e
@@ -6,9 +6,11 @@ module Pragma
6
6
  module Macro
7
7
  def self.Model(action = nil)
8
8
  step = lambda do |input, options|
9
+ klass = Macro.require_skill('Model', 'model.class', options)
10
+
9
11
  Trailblazer::Operation::Pipetree::Step.new(
10
- Trailblazer::Operation::Model.for(options['model.class'], action),
11
- 'model.class' => options['model.class'],
12
+ Trailblazer::Operation::Model.for(klass, action),
13
+ 'model.class' => klass,
12
14
  'model.action' => action
13
15
  ).call(input, options).tap do |result|
14
16
  unless result
@@ -10,7 +10,9 @@ module Pragma
10
10
  module Policy
11
11
  class << self
12
12
  def for(input, name, options, action = nil)
13
- policy = options["policy.#{name}.class"].new(
13
+ klass = Macro.require_skill('Policy', "policy.#{name}.class", options)
14
+
15
+ policy = klass.new(
14
16
  options['policy.context'] || options['current_user'],
15
17
  options['model']
16
18
  )
@@ -4,8 +4,6 @@ module Pragma
4
4
  module Operation
5
5
  # Finds all records of the requested resource, authorizes them, paginates them and decorates
6
6
  # them.
7
- #
8
- # @author Alessandro Desantis
9
7
  class Index < Pragma::Operation::Base
10
8
  step Macro::Classes()
11
9
  step :retrieve!, name: 'retrieve'
@@ -14,12 +12,19 @@ module Pragma
14
12
  step Macro::Ordering()
15
13
  step Macro::Pagination()
16
14
  step Macro::Decorator(name: :collection)
15
+ step :include!, name: 'include'
17
16
  step :respond!, name: 'respond'
18
17
 
19
18
  def retrieve!(options)
20
19
  options['model'] = options['model.class'].all
21
20
  end
22
21
 
22
+ def include!(options)
23
+ options['model'] = AssociationIncluder
24
+ .load_adaptor(options['model'])
25
+ .include_associations(options['params'][:expand] || [])
26
+ end
27
+
23
28
  # TODO: Turn this into a macro.
24
29
  def scope!(options, model:, **)
25
30
  options['model'] = options['policy.default.scope.class'].new(
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pragma
4
- VERSION = '2.4.0'
4
+ VERSION = '2.5.0'
5
5
  end
@@ -21,6 +21,7 @@ Gem::Specification.new do |spec|
21
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
22
  spec.require_paths = ['lib']
23
23
 
24
+ spec.add_dependency 'adaptor', '~> 0.2'
24
25
  spec.add_dependency 'pragma-contract', '~> 2.0'
25
26
  spec.add_dependency 'pragma-decorator', '~> 2.0'
26
27
  spec.add_dependency 'pragma-operation', '~> 2.0'
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pragma
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alessandro Desantis
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-06-09 00:00:00.000000000 Z
11
+ date: 2019-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: adaptor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.2'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: pragma-contract
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -197,6 +211,10 @@ files:
197
211
  - bin/console
198
212
  - bin/setup
199
213
  - lib/pragma.rb
214
+ - lib/pragma/association_includer.rb
215
+ - lib/pragma/association_includer/active_record.rb
216
+ - lib/pragma/association_includer/base.rb
217
+ - lib/pragma/association_includer/poro.rb
200
218
  - lib/pragma/decorator/error.rb
201
219
  - lib/pragma/filter/base.rb
202
220
  - lib/pragma/filter/boolean.rb
@@ -205,6 +223,7 @@ files:
205
223
  - lib/pragma/filter/like.rb
206
224
  - lib/pragma/filter/scope.rb
207
225
  - lib/pragma/filter/where.rb
226
+ - lib/pragma/macro.rb
208
227
  - lib/pragma/macro/classes.rb
209
228
  - lib/pragma/macro/contract/build.rb
210
229
  - lib/pragma/macro/contract/persist.rb
@@ -243,8 +262,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
243
262
  - !ruby/object:Gem::Version
244
263
  version: '0'
245
264
  requirements: []
246
- rubyforge_project:
247
- rubygems_version: 2.7.5
265
+ rubygems_version: 3.0.1
248
266
  signing_key:
249
267
  specification_version: 4
250
268
  summary: A pragmatic architecture for building JSON APIs with Ruby.