pragma 2.4.0 → 2.5.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.
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.