hub_kernel-interface 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 38b1b23f80d6d9309426a790f9f7e44d0ab7d3da6c092e5845a4a6ba7bbe9b36
4
+ data.tar.gz: 4e82d38b3c66e95e5ca4388d1ba45ef25e0b36984ed52bac9dc86c1cfa9fa45c
5
+ SHA512:
6
+ metadata.gz: ccdc332bfa7357d30249401bbef0e7fca543840bb3a580410a001c310aabbdbf7ddeea058ccee121da6e4915e1084eff5c82fabc62e3a7ffe88945c45fbd72d2
7
+ data.tar.gz: ab16e65c6a53358f99766428a2ff35ac46500a1cd0c0d4cbcf96177da11e89978793dc28d36b179b193b2dc37808140b8d2ea8b9d3faa156926971119dd8c202
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright tylercschneider
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # hub_kernel-interface
2
+
3
+ The exposed-method contract every hub_kernel interface gem builds on: what a hub exposes, who
4
+ may call it, and calling it in an account's scope. An interface gem, such as a JSON API or an
5
+ MCP server, depends on this gem alone rather than on all of hub_kernel.
6
+
7
+ ## Usage
8
+
9
+ A hub lists the methods outside callers may reach by name, the values each takes, and whether
10
+ it writes:
11
+
12
+ ```ruby
13
+ module Supplies
14
+ extend HubKernel::Exposes
15
+
16
+ exposes :price_of, takes: %i[item], writes: false
17
+
18
+ def self.price_of(item:) = "#{item} costs 3"
19
+ end
20
+ ```
21
+
22
+ The host sets the permission check, asked with the person, `hub:method` and the account, and
23
+ the account scope, wrapped around every call:
24
+
25
+ ```ruby
26
+ HubKernel::Authz.check = ->(person, action, account) { true }
27
+ HubKernel::Context.scope = ->(account, &call) { call.call }
28
+ ```
29
+
30
+ An interface gem lists the methods a person may call on an account, and calls one by name:
31
+
32
+ ```ruby
33
+ Supplies.exposures_for(person: person, account: account)
34
+ Supplies.call_exposed("price_of", values: { item: "soap" }, person: person, account: account)
35
+ ```
36
+
37
+ A call is refused with `HubKernel::UnexposedMethodError` for a name the hub does not expose,
38
+ `HubKernel::NotAllowed` when the permission check refuses, and `HubKernel::MissingArgumentError`
39
+ for a missing person, account or required value. A hub refuses with `HubKernel::Refused` and a
40
+ reason a person can read. A call made while the permission check or the account scope is unset
41
+ raises `HubKernel::UnwiredPortError`.
42
+
43
+ `Supplies.exposure_problems` names each exposed method the hub has no method for, or lists
44
+ with values it does not take. `HubKernel::Interface::ExposingHubs.list` holds every hub that
45
+ declares an exposed method.
46
+
47
+ ## Installation
48
+
49
+ ```ruby
50
+ gem "hub_kernel-interface"
51
+ ```
52
+
53
+ ## License
54
+
55
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/setup"
2
+ require "bundler/gem_tasks"
3
+ require "rake/testtask"
4
+
5
+ Rake::TestTask.new(:test) do |task|
6
+ task.libs << "test"
7
+ task.pattern = "test/**/*_test.rb"
8
+ end
9
+
10
+ task default: :test
@@ -0,0 +1,5 @@
1
+ module HubKernel
2
+ module Authz
3
+ singleton_class.attr_accessor :check
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module HubKernel
2
+ module Context
3
+ singleton_class.attr_accessor :scope
4
+ end
5
+ end
@@ -0,0 +1,95 @@
1
+ require "active_support/core_ext/string/inflections"
2
+ require "hub_kernel/authz"
3
+ require "hub_kernel/context"
4
+ require "hub_kernel/interface/exposing_hubs"
5
+
6
+ module HubKernel
7
+ class MissingArgumentError < ArgumentError; end
8
+ class UnwiredPortError < StandardError; end
9
+ class NonBooleanAnswerError < StandardError; end
10
+ class UnexposedMethodError < StandardError; end
11
+ class NotAllowed < StandardError; end
12
+ class Refused < StandardError; end
13
+
14
+ module Exposes
15
+ Exposed = Data.define(:name, :takes, :writes)
16
+
17
+ def exposes(name, takes:, writes:)
18
+ Interface::ExposingHubs.add(self)
19
+ exposed_methods[name.to_s] = Exposed.new(name: name, takes: takes, writes: writes)
20
+ end
21
+
22
+ def exposed(name) = exposed_methods[name.to_s]
23
+
24
+ def exposures = exposed_methods.values
25
+
26
+ def exposures_for(person:, account:)
27
+ refuse_without_caller(person, account)
28
+
29
+ exposures.select { |exposure| allowed?(exposure, person, account) }
30
+ end
31
+
32
+ def call_exposed(name, values:, person:, account:)
33
+ refuse_without_caller(person, account)
34
+
35
+ exposure = exposed(name) || raise(UnexposedMethodError, "#{exposing_hub} does not expose #{name}")
36
+ refuse_unless_allowed(exposure, person, account)
37
+ refuse_missing_values(exposure, values)
38
+ within_account(account) { public_send(exposure.name, **values.slice(*exposure.takes)) }
39
+ end
40
+
41
+ def exposure_problems
42
+ exposed_methods.values.filter_map { |exposure| exposure_problem(exposure) }
43
+ end
44
+
45
+ private
46
+
47
+ def exposed_methods = @exposed_methods ||= {}
48
+
49
+ def exposing_hub = name.demodulize
50
+
51
+ def refuse_without_caller(person, account)
52
+ raise MissingArgumentError, "A call by name needs a person" if person.nil?
53
+ raise MissingArgumentError, "A call by name needs an account" if account.nil?
54
+ end
55
+
56
+ def refuse_unless_allowed(exposure, person, account)
57
+ raise NotAllowed, "#{exposing_hub} #{exposure.name}" unless allowed?(exposure, person, account)
58
+ end
59
+
60
+ def allowed?(exposure, person, account)
61
+ raise UnwiredPortError, "hub_kernel's permission check is not filled" unless Authz.check
62
+
63
+ answer = Authz.check.call(person, "#{exposing_hub.underscore}:#{exposure.name}", account)
64
+ raise NonBooleanAnswerError, "The permission check must answer true or false, got #{answer.inspect}" unless [ true, false ].include?(answer)
65
+
66
+ answer
67
+ end
68
+
69
+ def within_account(account, &call)
70
+ raise UnwiredPortError, "hub_kernel's account scope is not filled" unless Context.scope
71
+
72
+ Context.scope.call(account, &call)
73
+ end
74
+
75
+ def refuse_missing_values(exposure, values)
76
+ missing = keywords(exposure, :keyreq) - values.keys
77
+ raise MissingArgumentError, "Give #{missing.join(", ")}" if missing.any?
78
+ end
79
+
80
+ def exposure_problem(exposure)
81
+ return "#{exposing_hub} exposes #{exposure.name}, which it has no method for" unless respond_to?(exposure.name)
82
+ return if takes_listed_values?(exposure)
83
+
84
+ "#{exposing_hub} exposes #{exposure.name} with #{exposure.takes.join(", ")}, but it takes #{keywords(exposure, :keyreq, :key).join(", ")}"
85
+ end
86
+
87
+ def takes_listed_values?(exposure)
88
+ (keywords(exposure, :keyreq) - exposure.takes).empty? && (takes_any_values?(exposure) || (exposure.takes - keywords(exposure, :keyreq, :key)).empty?)
89
+ end
90
+
91
+ def takes_any_values?(exposure) = method(exposure.name).parameters.any? { |kind, _| kind == :keyrest }
92
+
93
+ def keywords(exposure, *kinds) = method(exposure.name).parameters.filter_map { |kind, value| value if kinds.include?(kind) }
94
+ end
95
+ end
@@ -0,0 +1,12 @@
1
+ module HubKernel
2
+ module Interface
3
+ module ExposingHubs
4
+ def self.add(hub)
5
+ list.reject! { |listed| listed.name == hub.name }
6
+ list << hub
7
+ end
8
+
9
+ def self.list = @list ||= []
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,5 @@
1
+ module HubKernel
2
+ module Interface
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,2 @@
1
+ require "hub_kernel/interface/version"
2
+ require "hub_kernel/exposes"
@@ -0,0 +1 @@
1
+ require "hub_kernel/interface"
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hub_kernel-interface
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - tylercschneider
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activesupport
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 8.1.3
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 8.1.3
26
+ description: hub_kernel-interface holds what a hub exposes, who may call it, and calling
27
+ it in an account's scope, so an interface gem can serve hubs without depending on
28
+ the rest of hub_kernel.
29
+ email:
30
+ - tylercschneider@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - MIT-LICENSE
36
+ - README.md
37
+ - Rakefile
38
+ - lib/hub_kernel-interface.rb
39
+ - lib/hub_kernel/authz.rb
40
+ - lib/hub_kernel/context.rb
41
+ - lib/hub_kernel/exposes.rb
42
+ - lib/hub_kernel/interface.rb
43
+ - lib/hub_kernel/interface/exposing_hubs.rb
44
+ - lib/hub_kernel/interface/version.rb
45
+ homepage: https://github.com/DYB-Development/hub_kernel-interface
46
+ licenses:
47
+ - MIT
48
+ metadata:
49
+ allowed_push_host: https://rubygems.org
50
+ homepage_uri: https://github.com/DYB-Development/hub_kernel-interface
51
+ source_code_uri: https://github.com/DYB-Development/hub_kernel-interface
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 3.2.0
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 4.0.20
67
+ specification_version: 4
68
+ summary: The exposed-method contract every hub_kernel interface gem builds on.
69
+ test_files: []