foobara 0.5.6 → 0.5.8

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: ea16151696626b19708aa3c7a211785a28b2a9a507b5ef882985bea42d87aa55
4
- data.tar.gz: 32f4035f6239fc7d911baa3eaa1eefc9c064baa1d2848f3dd928d1d3a885ae70
3
+ metadata.gz: 7f3a7dee25a0ab1d7598ec72bbd71ec8807113d417cc758842cf4edd7b214d2a
4
+ data.tar.gz: f81bd84c70a9d41a8db414ee9424d5c2e2d9533887cfaa1ea0b436094873bc9a
5
5
  SHA512:
6
- metadata.gz: 40adb5c61122aab04ced114b0d380e052e0994ff6177d703bb34071763e83ac1bf7bdfcf41c803d6350e72d0085b1ce61a4377294925013189464142ff1b0ab1
7
- data.tar.gz: e9b55be0ed1beef28548794c3d383209ce1fa100cccb70fe46c604209300156687647a1f4fb3436fa38773c0e0381277b03cc61ef00442009b18fae713e71717
6
+ metadata.gz: 8819827dfdffb0ae2bef29f3bbbeb944cd3270cfd1a7938037d487373123ce50e2a8f832c5687c6b75ef8fd6e2ebd2016689e4b7208752a1f4a15bafa6eac10d
7
+ data.tar.gz: ba81595289f961c7c5f639d85fb16f7d05fe5b3d0aa3fcbeec1499030b7e56bbab75afc1cef58b6eafd5e6f115c3f2c4dbfb15ab29de8bc081095ccb4a8bd089
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ # [0.5.8] - 2026-03-04
2
+
3
+ - Provide a way for allowed rules to be declared independently of connecting commands
4
+
5
+ # [0.5.7] - 2026-02-25
6
+
7
+ - Add a Manifest::Model.associations convenience method
8
+ - Add Manifest::Tuple and various missing predicate methods
9
+ - Add a query flag to the manifest
10
+
1
11
  # [0.5.6] - 2026-02-20
2
12
 
3
13
  - Introduce an official Query concept
@@ -0,0 +1,77 @@
1
+ RSpec.describe Foobara::CommandConnector do
2
+ after { Foobara.reset_alls }
3
+
4
+ let(:authenticator) { -> { :some_user } }
5
+
6
+ let(:response) { command_connector.run(full_command_name:, action:, inputs:) }
7
+ let(:parsed_response) { JSON.parse(response.body) }
8
+
9
+ let(:action) { "run" }
10
+ let(:full_command_name) { "ComputeExponent" }
11
+ let(:inputs) do
12
+ { base:, exponent: }
13
+ end
14
+
15
+ let(:command_class) do
16
+ stub_class(:ComputeExponent, Foobara::Command) do
17
+ inputs do
18
+ exponent :integer, :required
19
+ base :integer, :required
20
+ end
21
+
22
+ result :integer
23
+
24
+ attr_accessor :exponential
25
+
26
+ def execute
27
+ compute
28
+
29
+ exponential
30
+ end
31
+
32
+ def compute
33
+ self.exponential = 1
34
+
35
+ exponent.times do
36
+ self.exponential *= base
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ context "when independently setting the allowed rules" do
43
+ let(:command_connector) do
44
+ described_class.new(authenticator:,
45
+ allow: { command_class => -> { base.even? } })
46
+ end
47
+
48
+ before do
49
+ command_connector.connect(command_class)
50
+ end
51
+
52
+ context "when allowed rule is met" do
53
+ let(:base) { 2 }
54
+ let(:exponent) { 3 }
55
+
56
+ it "runs the command" do
57
+ expect(response.status).to be(0)
58
+ expect(response.error).to be_nil
59
+ expect(response.body).to eq(8)
60
+ end
61
+ end
62
+
63
+ context "when allowed rule is not met" do
64
+ let(:base) { 3 }
65
+ let(:exponent) { 3 }
66
+
67
+ it "is not authorized" do
68
+ expect(response.status).to be(1)
69
+
70
+ errors = response.body
71
+
72
+ expect(errors.size).to eq(1)
73
+ expect(errors.first).to be_a(Foobara::CommandConnector::NotAllowedError)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -9,7 +9,7 @@ module Foobara
9
9
  def initialize(authenticators:, symbol: nil, explanation: nil, &block)
10
10
  self.authenticators = authenticators
11
11
 
12
- symbol ||= authenticators.map(&:symbol).map(&:to_s).join("_or_").to_sym
12
+ symbol ||= authenticators.map(&:symbol).join("_or_").to_sym
13
13
  explanation ||= authenticators.map(&:explanation).join(", or ")
14
14
 
15
15
  super(symbol:, explanation:)
@@ -199,7 +199,10 @@ module Foobara
199
199
  default_pre_commit_transformers: nil,
200
200
  auth_map: nil,
201
201
  current_user: nil,
202
+ # TODO: let's default this to true
202
203
  requires_allowed_rule: false,
204
+ allowed_rules: nil,
205
+ allow: nil,
203
206
  &block)
204
207
  authenticator = self.class.to_authenticator(authenticator)
205
208
 
@@ -216,6 +219,8 @@ module Foobara
216
219
 
217
220
  self.authenticator = authenticator
218
221
  self.command_registry = CommandRegistry.new(authenticator:)
222
+ allowed_rules&.each_pair { |symbol, rule| register_allowed_rule(symbol, rule) }
223
+ self.allow allow if allow
219
224
 
220
225
  self.capture_unknown_error = capture_unknown_error
221
226
  self.name = name
@@ -242,6 +247,10 @@ module Foobara
242
247
  end
243
248
  end
244
249
 
250
+ def allow(rules)
251
+ command_registry.allow rules
252
+ end
253
+
245
254
  def register_allowed_rule(*ruleish_args)
246
255
  command_registry.allowed_rule(*ruleish_args)
247
256
  end
@@ -632,7 +641,7 @@ module Foobara
632
641
  def run_command(request)
633
642
  command = request.command
634
643
 
635
- if requires_allowed_rule
644
+ if requires_allowed_rule && command.is_a?(TransformedCommand) && command.class.requires_authentication != false
636
645
  unless command.allowed_rule
637
646
  raise NoAllowedRuleGivenError,
638
647
  "Must connect #{command.full_command_name} with an `allowed_if:` " \
@@ -9,9 +9,10 @@ module Foobara
9
9
  # Should we support different authenticators for different commands?
10
10
  # Might be a smell of two domains co-habitating in one? Or maybe one is just
11
11
  # passing another through and we should support that?
12
- def initialize(authenticator: nil)
12
+ def initialize(authenticator: nil, allow: nil)
13
13
  self.scoped_path = []
14
14
  self.authenticator = authenticator
15
+ self.allow allow if allow
15
16
 
16
17
  customized = [:command]
17
18
 
@@ -193,15 +194,29 @@ module Foobara
193
194
  foobara_all_command.size
194
195
  end
195
196
 
197
+ def allow(rules = (no_args = true))
198
+ @allow ||= {}
199
+
200
+ if no_args
201
+ @allow
202
+ else
203
+ @allow = @allow.merge(rules)
204
+ end
205
+ end
206
+
196
207
  private
197
208
 
198
- def create_exposed_command(command_class, **)
209
+ def create_exposed_command(command_class, **opts)
199
210
  full_domain_name = command_class.domain.scoped_full_name
200
211
  exposed_domain = foobara_lookup_domain(full_domain_name,
201
212
  mode: Namespace::LookupMode::ABSOLUTE_SINGLE_NAMESPACE) ||
202
213
  build_and_register_exposed_domain(full_domain_name)
203
214
 
204
- exposed_command = create_exposed_command_without_domain(command_class, **)
215
+ if !opts.key?(:allowed_rule) && allow&.key?(command_class)
216
+ opts = opts.merge(allowed_rule: allow[command_class])
217
+ end
218
+
219
+ exposed_command = create_exposed_command_without_domain(command_class, **opts)
205
220
 
206
221
  exposed_domain.foobara_register(exposed_command)
207
222
 
@@ -292,7 +307,7 @@ module Foobara
292
307
  errors_transformers: nil,
293
308
  pre_commit_transformers: nil,
294
309
  serializers: nil,
295
- allowed_rule: default_allowed_rule,
310
+ allowed_rule: nil,
296
311
  authenticator: nil,
297
312
  **opts
298
313
  )
@@ -251,13 +251,11 @@ module Foobara
251
251
  contains_associations?(element_type, false)
252
252
  end
253
253
  elsif type.extends?(BuiltinTypes[:attributes])
254
-
255
254
  type.element_types&.values&.any? do |element_type|
256
255
  if !remove_sensitive || !element_type.sensitive?
257
256
  contains_associations?(element_type, false)
258
257
  end
259
258
  end
260
-
261
259
  elsif type.extends?(BuiltinTypes[:associative_array])
262
260
  element_types = type.element_types
263
261
 
@@ -18,7 +18,7 @@ module Foobara
18
18
  def desugarize(rawish_type_declaration)
19
19
  private = rawish_type_declaration[:private]
20
20
 
21
- if private.any? { |key| key.is_a?(::String) }
21
+ if private.any?(::String)
22
22
  rawish_type_declaration[:private] = private.map(&:to_sym)
23
23
  end
24
24
 
@@ -0,0 +1,67 @@
1
+ # TODO: Get this out of here and into model project
2
+ RSpec.describe Foobara::Manifest::Model do
3
+ after { Foobara.reset_alls }
4
+
5
+ describe ".associations" do
6
+ context "when the type has associations" do
7
+ before do
8
+ stub_class "SomeInnerInnerEntity", Foobara::Entity do
9
+ attributes do
10
+ id :integer
11
+ bar :string, :required
12
+ end
13
+ primary_key :id
14
+ end
15
+
16
+ stub_class "SomeOuterModel", Foobara::Model do
17
+ attributes do
18
+ some_inner_inner_entities [:SomeInnerInnerEntity, :SomeInnerInnerEntity]
19
+ end
20
+ end
21
+
22
+ stub_class("SomeInnerEntity", Foobara::Entity) do
23
+ attributes do
24
+ id :integer
25
+ foo :string, :required
26
+ end
27
+
28
+ primary_key :id
29
+ end
30
+
31
+ stub_class("SomeOuterEntity", Foobara::Entity) do
32
+ attributes do
33
+ id :integer
34
+ some_inner_entity :SomeInnerEntity
35
+ end
36
+
37
+ primary_key :id
38
+ end
39
+
40
+ Foobara::GlobalDomain.foobara_register_type(:some_type) do
41
+ some_array [SomeOuterEntity]
42
+ some_inner_model :SomeOuterModel
43
+ end
44
+ end
45
+
46
+ it "can reproduce association info from the underlying associations in the types" do
47
+ raw_manifest = Foobara.manifest
48
+
49
+ root_manifest = Foobara::Manifest::RootManifest.new(raw_manifest)
50
+
51
+ manifest_type = root_manifest.type_by_name(:some_type)
52
+
53
+ expect(manifest_type).to be_a(Foobara::Manifest::Type)
54
+
55
+ associations = described_class.associations(manifest_type)
56
+
57
+ expect(associations.keys).to contain_exactly(
58
+ "some_array.#",
59
+ "some_inner_model.some_inner_inner_entities.0",
60
+ "some_inner_model.some_inner_inner_entities.1"
61
+ )
62
+
63
+ expect(associations.values).to all be_a(Foobara::Manifest::Entity)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -71,7 +71,7 @@ RSpec.describe Foobara::Manifest do
71
71
  primary_key :id
72
72
  end
73
73
 
74
- stub_class "SomeOrg::SomeDomain::QueryUser", Foobara::Command
74
+ stub_class "SomeOrg::SomeDomain::QueryUser", Foobara::Query
75
75
  stub_class "SomeOrg::SomeDomain::QueryUser::SomethingWentWrongError", Foobara::RuntimeError do
76
76
  class << self
77
77
  def context_type_declaration
@@ -259,8 +259,11 @@ RSpec.describe Foobara::Manifest do
259
259
  expect(attributes.sensitive).to be_falsey
260
260
  expect(attributes.sensitive_exposed).to be_falsey
261
261
 
262
+ expect(manifest.queries.map(&:command_name)).to contain_exactly("QueryUser")
263
+
262
264
  command = manifest.command_by_name("SomeOrg::SomeDomain::QueryUser")
263
265
 
266
+ expect(command).to be_query
264
267
  expect(command).to be_a(Foobara::Manifest::Command)
265
268
  expect(command.scoped_category).to eq(:command)
266
269
  expect(command.parent_category).to eq(:domain)
@@ -0,0 +1,30 @@
1
+ RSpec.describe Foobara::Manifest::Type do
2
+ after { Foobara.reset_alls }
3
+
4
+ context "when it's an associative array" do
5
+ before do
6
+ Foobara::GlobalDomain.foobara_register_type(:some_type,
7
+ type: :associative_array,
8
+ value_type_declaration: :string,
9
+ key_type_declaration: :integer)
10
+ end
11
+
12
+ describe "#associative_array?" do
13
+ it "is true" do
14
+ root_manifest = Foobara::Manifest::RootManifest.new(Foobara.manifest)
15
+ expect(root_manifest.type_by_name(:some_type)).to be_associative_array
16
+ end
17
+ end
18
+
19
+ describe "#to_type_declaration_from_declaration_data" do
20
+ it "gives an associative_array declaration" do
21
+ root_manifest = Foobara::Manifest::RootManifest.new(Foobara.manifest)
22
+ type = root_manifest.type_by_name(:some_type)
23
+ declaration = type.to_type_declaration_from_declaration_data
24
+
25
+ expect(declaration).to be_a(Foobara::Manifest::TypeDeclaration)
26
+ expect(declaration).to be_associative_array
27
+ end
28
+ end
29
+ end
30
+ end
@@ -69,8 +69,6 @@ module Foobara
69
69
  end
70
70
 
71
71
  def domain
72
- domain_reference = self[:domain]
73
-
74
72
  Domain.new(root_manifest, [:domain, domain_reference])
75
73
  end
76
74
 
@@ -21,6 +21,10 @@ module Foobara
21
21
  scoped_full_name
22
22
  end
23
23
 
24
+ def query?
25
+ self[:is_query]
26
+ end
27
+
24
28
  def inputs_type
25
29
  Attributes.new(root_manifest, [*manifest_path, :inputs_type])
26
30
  end
@@ -3,6 +3,44 @@ require_relative "type"
3
3
  module Foobara
4
4
  module Manifest
5
5
  class Model < Type
6
+ class << self
7
+ def associations(type, path = DataPath.new, result = {}, initial: true)
8
+ if type.detached_entity? && !initial
9
+ type = type.to_type if type.is_a?(TypeDeclaration)
10
+ result[path.to_s] = type
11
+ elsif type.model?
12
+ type = type.to_type if type.is_a?(TypeDeclaration)
13
+ associations(type.attributes_type, path, result, initial: false)
14
+ elsif type.tuple?
15
+ type = type.to_type_declaration_from_declaration_data if type.is_a?(Type)
16
+ type.element_types&.each&.with_index do |element_type, index|
17
+ associations(element_type, path.append(index), result, initial: false)
18
+ end
19
+ elsif type.array?
20
+ type = type.to_type_declaration_from_declaration_data if type.is_a?(Type)
21
+ element_type = type.element_type
22
+
23
+ if element_type
24
+ associations(element_type, path.append(:"#"), result, initial: false)
25
+ end
26
+ elsif type.attributes?
27
+ type = type.to_type_declaration_from_declaration_data if type.is_a?(Type)
28
+ type.attribute_declarations.each_pair do |attribute_name, element_type|
29
+ associations(element_type, path.append(attribute_name), result, initial: false)
30
+ end
31
+ # :nocov:
32
+ elsif type.associative_array?
33
+ if contains_associations?(type)
34
+ raise "Associative array types with associations in them are not currently supported. " \
35
+ "Use attributes type if you can or set the key_type and/or value_type to duck type"
36
+ end
37
+ end
38
+ # :nocov:
39
+
40
+ result
41
+ end
42
+ end
43
+
6
44
  self.category_symbol = :type
7
45
 
8
46
  optional_keys :delegates
@@ -25,6 +25,10 @@ module Foobara
25
25
  organizations.map(&:commands).flatten
26
26
  end
27
27
 
28
+ def queries
29
+ commands.select(&:query?)
30
+ end
31
+
28
32
  def types
29
33
  @types ||= DataPath.value_at(:type, root_manifest).keys.map do |reference|
30
34
  Type.new(root_manifest, [:type, reference])
@@ -0,0 +1,15 @@
1
+ require_relative "type_declaration"
2
+
3
+ module Foobara
4
+ module Manifest
5
+ class Tuple < TypeDeclaration
6
+ alias tuple_manifest relevant_manifest
7
+
8
+ def element_types
9
+ @element_types ||= (0...element_type_declarations.size).map do |index|
10
+ TypeDeclaration.new(root_manifest, [*manifest_path, :element_type_declarations, index])
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -38,6 +38,22 @@ module Foobara
38
38
  !builtin? && extends_symbol?(:model)
39
39
  end
40
40
 
41
+ def array?
42
+ !builtin? && extends_symbol?(:array)
43
+ end
44
+
45
+ def attributes?
46
+ !builtin? && extends_symbol?(:attributes)
47
+ end
48
+
49
+ def associative_array?
50
+ !builtin? && extends_symbol?(:associative_array)
51
+ end
52
+
53
+ def tuple?
54
+ !builtin? && extends_symbol?(:tuple)
55
+ end
56
+
41
57
  def base_type
42
58
  base_type_symbol = self[:base_type]
43
59
 
@@ -15,6 +15,8 @@ module Foobara
15
15
  Attributes.new(type_declaration.root_manifest, type_declaration.manifest_path)
16
16
  when :array
17
17
  Array.new(type_declaration.root_manifest, type_declaration.manifest_path)
18
+ when :tuple
19
+ Tuple.new(type_declaration.root_manifest, type_declaration.manifest_path)
18
20
  else
19
21
  type_declaration
20
22
  end
@@ -82,6 +84,14 @@ module Foobara
82
84
  type.to_sym == :array
83
85
  end
84
86
 
87
+ def tuple?
88
+ type.to_sym == :tuple
89
+ end
90
+
91
+ def associative_array?
92
+ type.to_sym == :associative_array
93
+ end
94
+
85
95
  def model?
86
96
  return @model if defined?(@model)
87
97
 
@@ -83,9 +83,13 @@ module Foobara
83
83
  end
84
84
 
85
85
  def errors_hash
86
- each_with_object({}) do |error, hash|
87
- hash[error.key] = error.to_h
86
+ h = {}
87
+
88
+ each do |error|
89
+ h[error.key] = error.to_h
88
90
  end
91
+
92
+ h
89
93
  end
90
94
 
91
95
  def to_sentence
data/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Foobara
2
2
  module Version
3
- VERSION = "0.5.6".freeze
3
+ VERSION = "0.5.8".freeze
4
4
  MINIMUM_RUBY_VERSION = ">= 3.4.0".freeze
5
5
  end
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foobara
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.6
4
+ version: 0.5.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi
@@ -106,6 +106,7 @@ files:
106
106
  - projects/command/src/state_machine.rb
107
107
  - projects/command_connectors/Guardfile
108
108
  - projects/command_connectors/lib/foobara/command_connectors.rb
109
+ - projects/command_connectors/spec/allowed_rules_spec.rb
109
110
  - projects/command_connectors/spec/ambiguous_lookups_spec.rb
110
111
  - projects/command_connectors/spec/auth_mappers_spec.rb
111
112
  - projects/command_connectors/spec/command_connector_spec.rb
@@ -340,10 +341,12 @@ files:
340
341
  - projects/entities_plumbing/src/extensions/authenticator.rb
341
342
  - projects/manifest/Guardfile
342
343
  - projects/manifest/lib/foobara/manifest.rb
344
+ - projects/manifest/spec/associations_spec.rb
343
345
  - projects/manifest/spec/manifest_spec.rb
344
346
  - projects/manifest/spec/model_spec.rb
345
347
  - projects/manifest/spec/spec_helper.rb
346
348
  - projects/manifest/spec/type_declaration_spec.rb
349
+ - projects/manifest/spec/type_spec.rb
347
350
  - projects/manifest/src/array.rb
348
351
  - projects/manifest/src/attributes.rb
349
352
  - projects/manifest/src/base_manifest.rb
@@ -358,6 +361,7 @@ files:
358
361
  - projects/manifest/src/processor.rb
359
362
  - projects/manifest/src/processor_class.rb
360
363
  - projects/manifest/src/root_manifest.rb
364
+ - projects/manifest/src/tuple.rb
361
365
  - projects/manifest/src/type.rb
362
366
  - projects/manifest/src/type_declaration.rb
363
367
  - projects/model_plumbing/lib/foobara/model_plumbing.rb