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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +55 -0
- data/Rakefile +10 -0
- data/lib/hub_kernel/authz.rb +5 -0
- data/lib/hub_kernel/context.rb +5 -0
- data/lib/hub_kernel/exposes.rb +95 -0
- data/lib/hub_kernel/interface/exposing_hubs.rb +12 -0
- data/lib/hub_kernel/interface/version.rb +5 -0
- data/lib/hub_kernel/interface.rb +2 -0
- data/lib/hub_kernel-interface.rb +1 -0
- metadata +69 -0
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,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 @@
|
|
|
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: []
|