ruby-object-framework 1.0.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/API.md +78 -0
- data/README.md +250 -0
- data/THIRD_PARTY_NOTICES.md +45 -0
- data/examples/guides/01_plain_use_case.rb +13 -0
- data/examples/guides/02_composed_application.rb +25 -0
- data/examples/guides/03_order_domain.rb +14 -0
- data/examples/guides/04_scoped_resources.rb +38 -0
- data/examples/guides/05_mapping.rb +39 -0
- data/examples/guides/06_associations.rb +49 -0
- data/examples/guides/07_http.rb +17 -0
- data/examples/guides/08_errors.rb +20 -0
- data/examples/guides/09_sqlite.rb +38 -0
- data/examples/guides/10_grpc.rb +26 -0
- data/examples/guides/11_ractor.rb +15 -0
- data/examples/guides/12_box.rb +11 -0
- data/examples/guides/13_testing.rb +24 -0
- data/examples/guides/manifest.yml +62 -0
- data/examples/orders/application.rb +51 -0
- data/examples/orders/contracts.rb +19 -0
- data/examples/orders/domain.rb +35 -0
- data/examples/orders/export_proto.rb +5 -0
- data/examples/orders/grpc_client.py +24 -0
- data/examples/orders/grpc_server.rb +23 -0
- data/examples/orders/server.rb +18 -0
- data/lib/object_framework/application.rb +136 -0
- data/lib/object_framework/capabilities.rb +59 -0
- data/lib/object_framework/errors.rb +85 -0
- data/lib/object_framework/execution.rb +184 -0
- data/lib/object_framework/grpc/messages.rb +30 -0
- data/lib/object_framework/grpc/server.rb +264 -0
- data/lib/object_framework/grpc/service.rb +55 -0
- data/lib/object_framework/http/io.rb +127 -0
- data/lib/object_framework/http/presentation.rb +55 -0
- data/lib/object_framework/http/protocol.rb +209 -0
- data/lib/object_framework/http/router.rb +80 -0
- data/lib/object_framework/http/server.rb +279 -0
- data/lib/object_framework/http2/connection.rb +399 -0
- data/lib/object_framework/http2/frame.rb +21 -0
- data/lib/object_framework/http2/hpack.rb +206 -0
- data/lib/object_framework/http2/hpack_tables.rb +333 -0
- data/lib/object_framework/http2/inbound.rb +75 -0
- data/lib/object_framework/memory/storage.rb +97 -0
- data/lib/object_framework/persistence/mapping.rb +150 -0
- data/lib/object_framework/persistence/query.rb +97 -0
- data/lib/object_framework/persistence/session.rb +173 -0
- data/lib/object_framework/persistence/transaction.rb +54 -0
- data/lib/object_framework/protobuf.rb +268 -0
- data/lib/object_framework/sqlite/compiler.rb +77 -0
- data/lib/object_framework/sqlite/schema.rb +95 -0
- data/lib/object_framework/sqlite/storage.rb +220 -0
- data/lib/object_framework/sqlite.rb +6 -0
- data/lib/object_framework/version.rb +5 -0
- data/lib/object_framework.rb +31 -0
- metadata +105 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a1e2d4ec32fc727f2bf3ace15ac7d7b258b4ebcbe8ec3b3e23402138c45225e6
|
|
4
|
+
data.tar.gz: e86edf614e5007fb417230d2fc590f5f1a3b356bea297301cba9154e7e31d9b5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b3afe27f57481683738558c93ad8c32b48b002eb26b271bec74efff482c4523a374a6aeeff0e0c12725ef569f408c9239629aad069a21efa10971269b2dd0262
|
|
7
|
+
data.tar.gz: a67817b8b87c0b5b0486deb700bce6670f8a2c43a70bb26a2e8122446c85d51884073827e3a4679fd45d54beb74ee33b223324a4d5168809bae425758fad5e25
|
data/API.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# ObjectFramework API
|
|
2
|
+
|
|
3
|
+
This reference is generated from the Ruby source in `lib/`. It describes the
|
|
4
|
+
objects you compose, their method signatures, and the contracts callers must
|
|
5
|
+
honor. For a progressive introduction and complete runnable applications, read
|
|
6
|
+
the [ObjectFramework Guides](https://gstn-caruso.github.io/ruby-object-framework/).
|
|
7
|
+
|
|
8
|
+
## Application composition and execution
|
|
9
|
+
|
|
10
|
+
Start with [Application](rdoc-ref:ObjectFramework::Application). Register
|
|
11
|
+
invocation-scoped collaborators with `factory`, declare use cases with
|
|
12
|
+
`operation`, and finalize the configuration with `start`. Always call `stop`
|
|
13
|
+
in an `ensure` block.
|
|
14
|
+
|
|
15
|
+
Each operation declares `policy: :none`, `:read`, or `:write`. Transactional
|
|
16
|
+
operations require storage. Dependencies are explicit constructor keywords;
|
|
17
|
+
`context`, `session`, and `transaction` are reserved execution dependencies.
|
|
18
|
+
The latter two require a transactional operation, and sessions require mappings.
|
|
19
|
+
|
|
20
|
+
[ExecutionContext](rdoc-ref:ObjectFramework::ExecutionContext) carries a generated
|
|
21
|
+
operation identifier and [Cancellation](rdoc-ref:ObjectFramework::Cancellation).
|
|
22
|
+
Results must contain independent values: strings, numbers, booleans, symbols,
|
|
23
|
+
nil, arrays, hashes, and `Data` objects composed of these. They are copied and
|
|
24
|
+
frozen before flush and commit. Cycles and unsupported objects raise
|
|
25
|
+
[UnsafeResult](rdoc-ref:ObjectFramework::UnsafeResult).
|
|
26
|
+
|
|
27
|
+
## Persistence
|
|
28
|
+
|
|
29
|
+
[Mappings](rdoc-ref:ObjectFramework::Persistence::Mappings) describes ordinary
|
|
30
|
+
domain objects through [Mapping](rdoc-ref:ObjectFramework::Persistence::Mapping).
|
|
31
|
+
Each mapping needs an `id` field stored in the `id` column. Associations load
|
|
32
|
+
only when explicitly requested through a session's `load:` plan.
|
|
33
|
+
|
|
34
|
+
[Session](rdoc-ref:ObjectFramework::Persistence::Session) owns identity and change
|
|
35
|
+
tracking within an invocation. Use `add`, `find`, `query`, and `remove` inside
|
|
36
|
+
the use case; return values instead of managed entities. The invocation flushes
|
|
37
|
+
the session, commits, and closes its resources. A closed session cannot be reused.
|
|
38
|
+
|
|
39
|
+
[Query](rdoc-ref:ObjectFramework::Persistence::Query) is immutable. Each filter,
|
|
40
|
+
comparison, ordering, limit, or offset returns a new query. Queries run with
|
|
41
|
+
[Memory::Storage](rdoc-ref:ObjectFramework::Memory::Storage) or the optional
|
|
42
|
+
[SQLite::Storage](rdoc-ref:ObjectFramework::SQLite::Storage). SQLite requires the
|
|
43
|
+
`sqlite3` gem; [SQLite::Schema](rdoc-ref:ObjectFramework::SQLite::Schema) derives
|
|
44
|
+
tables from mappings and accepts consecutive migrations before storage startup.
|
|
45
|
+
Commit failures are never automatically retried.
|
|
46
|
+
|
|
47
|
+
## HTTP, protobuf, and gRPC
|
|
48
|
+
|
|
49
|
+
[HTTP::Router](rdoc-ref:ObjectFramework::HTTP::Router) binds method/path patterns
|
|
50
|
+
to explicit controllers. Install [HTTP::Server](rdoc-ref:ObjectFramework::HTTP::Server)
|
|
51
|
+
in the application to serve HTTP/1.1 with bounded admission and request timeouts.
|
|
52
|
+
[HTTP::JSONPresenter](rdoc-ref:ObjectFramework::HTTP::JSONPresenter) formats values;
|
|
53
|
+
[HTTP::ProblemPresenter](rdoc-ref:ObjectFramework::HTTP::ProblemPresenter) formats
|
|
54
|
+
safe error descriptions as problem details.
|
|
55
|
+
|
|
56
|
+
[Protobuf::Message](rdoc-ref:ObjectFramework::Protobuf::Message) declares proto3
|
|
57
|
+
fields and encodes or decodes ordinary hashes.
|
|
58
|
+
[GRPC::Service](rdoc-ref:ObjectFramework::GRPC::Service) declares unary and
|
|
59
|
+
streaming methods and exports their `.proto` contract. Install
|
|
60
|
+
[GRPC::Server](rdoc-ref:ObjectFramework::GRPC::Server) with service descriptors
|
|
61
|
+
and controllers. Streaming controllers invoke operations per message or batch,
|
|
62
|
+
then yield independent values after their transactions finish.
|
|
63
|
+
|
|
64
|
+
## Failures and optional capabilities
|
|
65
|
+
|
|
66
|
+
Register domain exceptions in [ErrorRegistry](rdoc-ref:ObjectFramework::ErrorRegistry)
|
|
67
|
+
before startup. The closest registered ancestor supplies the public category,
|
|
68
|
+
code, title, and explicitly safe details. Unexpected exceptions receive a generic
|
|
69
|
+
description. [FailureReporter](rdoc-ref:ObjectFramework::FailureReporter) sends
|
|
70
|
+
immutable diagnostic records to a callable sink without exception messages or
|
|
71
|
+
input values.
|
|
72
|
+
|
|
73
|
+
[Capabilities::Definitions](rdoc-ref:ObjectFramework::Capabilities::Definitions)
|
|
74
|
+
loads definitions into a Ruby Box when Ruby starts with `RUBY_BOX=1`.
|
|
75
|
+
[Capabilities::IsolatedTask](rdoc-ref:ObjectFramework::Capabilities::IsolatedTask)
|
|
76
|
+
runs bounded computations over independent values in a new Ractor. Cancellation
|
|
77
|
+
checks happen before and after that computation; they do not forcibly terminate
|
|
78
|
+
the Ractor. These capabilities are optional.
|
data/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# ObjectFramework
|
|
2
|
+
|
|
3
|
+
A modular Ruby framework for applications built from collaborating objects.
|
|
4
|
+
Use cases are ordinary Ruby classes, dependencies are explicit, and each
|
|
5
|
+
invocation owns its resources and transaction.
|
|
6
|
+
|
|
7
|
+
Read the [ObjectFramework Guides](https://gstn-caruso.github.io/ruby-object-framework/)
|
|
8
|
+
for progressive tutorials, executable examples, and the reasoning behind the
|
|
9
|
+
framework's design. Start with [Your first application](https://gstn-caruso.github.io/ruby-object-framework/getting_started.html).
|
|
10
|
+
|
|
11
|
+
Development follows the six deliveries in [PLAN.md](PLAN.md). See the
|
|
12
|
+
[implementation record](IMPLEMENTATION.md) for the current delivery status.
|
|
13
|
+
The initial implementation includes HTTP/1.1, native gRPC streaming, HashMap and
|
|
14
|
+
SQLite persistence, and optional Ruby execution capabilities.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Install the gem on Ruby 4.0.8:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
gem install ruby-object-framework
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or add it to your application's Gemfile:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem "ruby-object-framework", require: "object_framework"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Load the framework with `require "object_framework"`. The gem includes all
|
|
31
|
+
framework modules and runnable examples. SQLite remains optional; applications
|
|
32
|
+
using that adapter must also declare `gem "sqlite3", "~> 2.10", ">= 2.10.1"`.
|
|
33
|
+
|
|
34
|
+
## Automatic releases
|
|
35
|
+
|
|
36
|
+
After verification succeeds on `main`, semantic-release updates the Ruby version
|
|
37
|
+
and lockfile, creates a version tag, publishes to RubyGems.org, and attaches the
|
|
38
|
+
gem and release notes to a GitHub release. Breaking changes increment major,
|
|
39
|
+
`feat` increments minor, and other changes increment patch, including docs-only
|
|
40
|
+
merges. The first automated release is `1.0.0`.
|
|
41
|
+
|
|
42
|
+
See [release setup and recovery](docs/RELEASING.md) for the one-time RubyGems
|
|
43
|
+
trusted-publisher registration and local packaging commands.
|
|
44
|
+
|
|
45
|
+
## Requirements and tests
|
|
46
|
+
|
|
47
|
+
Ruby **4.0.8** is required. Version files support asdf and other Ruby managers.
|
|
48
|
+
The core and test runner use Ruby core and the standard library.
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
ruby -Ilib:test test/run.rb
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
For the full interoperability gate, prepare the independent Python clients:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
bundle install
|
|
58
|
+
python3 -m venv .venv
|
|
59
|
+
.venv/bin/python -m pip install -r test/interop/requirements.txt
|
|
60
|
+
ruby -Ilib:test test/run.rb
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Python dependencies are test clients; the framework does not load them. Set
|
|
64
|
+
`OBJECT_FRAMEWORK_INTEROP_PYTHON` to use an existing test environment.
|
|
65
|
+
|
|
66
|
+
Pass test filenames to run selected scenarios:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
ruby -Ilib:test test/run.rb test/core_test.rb test/invocation_test.rb
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Release tooling requires Node.js 24.10 or newer and is only used in development
|
|
73
|
+
and CI. Verify its version rules and release cycle with:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
npm ci --ignore-scripts
|
|
77
|
+
npm test
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## An ordinary use case
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
require "object_framework"
|
|
84
|
+
|
|
85
|
+
class Greet
|
|
86
|
+
def initialize(greeting:)
|
|
87
|
+
@greeting = greeting
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def call(name:)
|
|
91
|
+
{ message: "#{@greeting}, #{name}" }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
app = ObjectFramework::Application.new
|
|
96
|
+
app.factory(:greeting) { "Hello" }
|
|
97
|
+
app.operation(:greet, use_case: Greet, policy: :none, dependencies: [:greeting])
|
|
98
|
+
app.start
|
|
99
|
+
begin
|
|
100
|
+
app.invoke(:greet, name: "Ada") # => { message: "Hello, Ada" }
|
|
101
|
+
ensure
|
|
102
|
+
app.stop
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Every operation declares `:none`, `:read`, or `:write` as its transaction policy.
|
|
107
|
+
Transactional operations require a storage adapter. Factories are scoped to an
|
|
108
|
+
invocation, may declare their own dependencies, and may opt into cleanup with
|
|
109
|
+
`close: true`. The reserved `context` dependency exposes a server-generated
|
|
110
|
+
operation identifier and cooperative cancellation. Application registrations
|
|
111
|
+
are finalized at startup. Separate application instances have separate registries.
|
|
112
|
+
|
|
113
|
+
Public results contain independent, immutable values: strings, numbers,
|
|
114
|
+
booleans, symbols, nil, arrays, hashes, and `Data` objects composed of these.
|
|
115
|
+
Returning an unsupported object or a cycle fails before commit. Managed domain
|
|
116
|
+
objects must be converted to values inside the use case.
|
|
117
|
+
|
|
118
|
+
## Errors and diagnostics
|
|
119
|
+
|
|
120
|
+
Register ordinary exception classes through `app.errors.register`, supplying a
|
|
121
|
+
category, stable public code, title, and an optional explicit safe-details
|
|
122
|
+
extractor. The closest registered ancestor wins. Unexpected failures receive a
|
|
123
|
+
generic public description.
|
|
124
|
+
|
|
125
|
+
`FailureReporter` accepts a `sink:` callable. It records one primary failure per
|
|
126
|
+
interaction, plus cleanup failures marked as secondary. Records contain the
|
|
127
|
+
operation identifier, public code, exception and cause classes, backtrace,
|
|
128
|
+
duration, and transaction outcome. Exception messages and input values are
|
|
129
|
+
excluded to avoid exposing credentials or payloads. Applications should apply
|
|
130
|
+
their own retention and access policy to the sink.
|
|
131
|
+
|
|
132
|
+
Commit outcomes remain `:committed`, `:rolled_back`, or `:indeterminate` when
|
|
133
|
+
the adapter can determine them. A failed commit is never automatically retried.
|
|
134
|
+
Cancellation is cooperative; use cases doing long work should call
|
|
135
|
+
`context.cancellation.check!` at suitable boundaries.
|
|
136
|
+
|
|
137
|
+
## Contributing
|
|
138
|
+
|
|
139
|
+
Keep public APIs, tests, documentation, and explanatory comments in English.
|
|
140
|
+
Start a behavior change with a failing scenario in the dependency-free runner,
|
|
141
|
+
then implement it and run the current delivery gate. Comments explaining
|
|
142
|
+
protocol constraints and non-obvious decisions are welcome.
|
|
143
|
+
|
|
144
|
+
## HTTP order example
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
ruby -Ilib examples/orders/server.rb
|
|
148
|
+
curl -X POST http://127.0.0.1:8080/orders/ready/confirm
|
|
149
|
+
curl -X POST http://127.0.0.1:8080/orders/missing/confirm
|
|
150
|
+
curl -X POST http://127.0.0.1:8080/orders/rejected/confirm
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`ConfirmOrder` collaborates with an injected repository. `Order` owns its
|
|
154
|
+
confirmation invariant. An external mapping describes persistence, and an HTTP
|
|
155
|
+
controller normalizes path parameters before invoking the operation. The test
|
|
156
|
+
suite also demonstrates a concurrent commit conflict and an unexpected storage
|
|
157
|
+
failure. The example uses volatile in-memory storage.
|
|
158
|
+
|
|
159
|
+
HTTP uses a dispatcher with `IO.select` and Fibers, then one new thread per
|
|
160
|
+
admitted request. Admission defaults to 128 active requests. Keep-alive requests
|
|
161
|
+
run sequentially on separate threads. Limits default to an 8 KiB request line,
|
|
162
|
+
32 KiB header section, and 1 MiB body. Header timeout is 10 seconds; body,
|
|
163
|
+
execution, write, and idle timeouts are 30 seconds. Execution cancellation is
|
|
164
|
+
cooperative. `Server` accepts an application-configured OpenSSL `tls_context:`
|
|
165
|
+
with a certificate and private key.
|
|
166
|
+
|
|
167
|
+
The parser follows [RFC 9112](https://www.rfc-editor.org/rfc/rfc9112.html).
|
|
168
|
+
Ambiguous framing closes the connection. Invalid JSON yields 400, invalid
|
|
169
|
+
structure yields 422, missing routes yield 404, and wrong methods yield 405 with
|
|
170
|
+
`Allow`. Errors use [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html)
|
|
171
|
+
problem details; presentation and send failures preserve the transaction outcome.
|
|
172
|
+
|
|
173
|
+
## SQLite
|
|
174
|
+
|
|
175
|
+
Install the sole optional runtime gem with `bundle install` (or install `sqlite3`
|
|
176
|
+
2.10.1 or newer in the 2.x series). Requiring `object_framework` does not load
|
|
177
|
+
sqlite3 until `ObjectFramework::SQLite` is used.
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
schema = ObjectFramework::SQLite::Schema.from_mappings(mappings)
|
|
181
|
+
storage = ObjectFramework::SQLite::Storage.new(path: "orders.sqlite3", schema:)
|
|
182
|
+
app = ObjectFramework::Application.new(storage:, mappings:)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Each invocation owns one SQLite connection. Foreign keys are enabled, journal
|
|
186
|
+
mode is DELETE, and synchronization is FULL. Writes use IMMEDIATE transactions;
|
|
187
|
+
reads use DEFERRED transactions. Lock waiting respects the execution deadline
|
|
188
|
+
and a configurable `lock_timeout:` (one second by default). Writes have no
|
|
189
|
+
automatic application-level retries. SQL values are bound parameters; table
|
|
190
|
+
and column names must belong to the declared schema.
|
|
191
|
+
|
|
192
|
+
Schema version 1 creates the declared tables. Add consecutive versions before
|
|
193
|
+
constructing the storage:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
schema.migration(2) { add_column :orders, :note, type: :string }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Migrations run in a transaction and use SQLite's `user_version`. Startup rejects
|
|
200
|
+
newer database versions and mismatched column declarations. DELETE journaling
|
|
201
|
+
can delay a writer's commit while a reader remains active; keep invocations
|
|
202
|
+
short. Call `storage.close` after stopping applications that use it.
|
|
203
|
+
|
|
204
|
+
## gRPC
|
|
205
|
+
|
|
206
|
+
Run the same order use case through HTTP and gRPC:
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
ruby -Ilib examples/orders/grpc_server.rb
|
|
210
|
+
ruby -Ilib examples/orders/export_proto.rb > orders.proto
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The gRPC service is `orders.Orders/ConfirmOrder`. The framework implements
|
|
214
|
+
HTTP/2, HPACK, proto3 wire encoding, gRPC framing, and trailers in Ruby. Message
|
|
215
|
+
and service descriptors are Ruby objects that export `.proto` contracts.
|
|
216
|
+
Unary, server streaming, client streaming, and bidirectional calls each receive
|
|
217
|
+
one new request thread. Streaming controllers invoke use cases per message or
|
|
218
|
+
batch so that sessions and transactions finish before yielding public values.
|
|
219
|
+
|
|
220
|
+
Requests default to a 1 MiB message limit, a 32 KiB header limit, 128 active RPCs,
|
|
221
|
+
and 128 connections. Both connection and stream windows bound inbound buffering
|
|
222
|
+
and outgoing DATA. Deadlines and `RST_STREAM` cancellation reach the explicit
|
|
223
|
+
context. Responses carry `grpc-status`, a safe `grpc-message` when needed,
|
|
224
|
+
`error-code`, and `operation-id`. Message compression currently uses identity.
|
|
225
|
+
TLS accepts application certificates and negotiates `h2` through ALPN.
|
|
226
|
+
|
|
227
|
+
Interop tests use Python grpcio, Google's generated protobuf codec, and
|
|
228
|
+
hyper-h2. They cover every RPC mode, every scalar type, large messages, header
|
|
229
|
+
continuations, error metadata, TLS, admission, deadlines, cancellation, and
|
|
230
|
+
failure after a stream has produced values. Protocol references are
|
|
231
|
+
[HTTP/2](https://www.rfc-editor.org/rfc/rfc9113.html),
|
|
232
|
+
[HPACK](https://www.rfc-editor.org/rfc/rfc7541.html), and the
|
|
233
|
+
[gRPC wire protocol](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md).
|
|
234
|
+
|
|
235
|
+
## Optional Ruby capabilities
|
|
236
|
+
|
|
237
|
+
`ObjectFramework::Capabilities::Definitions` wraps Ruby Box. Start Ruby with
|
|
238
|
+
`RUBY_BOX=1`; enabling the variable later cannot change the running VM.
|
|
239
|
+
Separate instances load their own application definitions. Ruby Box remains
|
|
240
|
+
experimental, so the ordinary composition path does not require it.
|
|
241
|
+
|
|
242
|
+
`Capabilities::IsolatedTask.new(task: YourTask)` runs a class/module's
|
|
243
|
+
`self.call(input)` in a new Ractor. Inputs and results are independent values;
|
|
244
|
+
sessions and execution contexts stay on the owning request thread. Tasks should
|
|
245
|
+
be bounded computations over values. Cancellation is checked before and after
|
|
246
|
+
the isolated task; it does not forcibly terminate a running Ractor.
|
|
247
|
+
|
|
248
|
+
`ruby benchmark/compare.rb 20000 3` compares interpreter, YJIT, and ZJIT in
|
|
249
|
+
separate processes. See [the benchmark notes](benchmark/README.md) for the
|
|
250
|
+
workload, raw results, and limits of the comparison.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
## Documentation fonts
|
|
4
|
+
|
|
5
|
+
The guides self-host Inter and IBM Plex Mono under the SIL Open Font License
|
|
6
|
+
1.1. Their copyright notices and complete licenses are distributed alongside
|
|
7
|
+
the font files in `docs/assets/fonts/Inter-LICENSE.txt` and
|
|
8
|
+
`docs/assets/fonts/Plex-LICENSE.txt`.
|
|
9
|
+
|
|
10
|
+
- Inter: https://github.com/rsms/inter, revision
|
|
11
|
+
`353b61b9f4430d5f420d56605a6e7993e0941470`.
|
|
12
|
+
- IBM Plex: https://github.com/IBM/plex, revision
|
|
13
|
+
`71f558992315a12ca1f06a2215d57962a1992162`.
|
|
14
|
+
|
|
15
|
+
The documentation stylesheet is an original implementation of the visual
|
|
16
|
+
reference requested by the project: https://guides.rubyonrails.org/index.html.
|
|
17
|
+
|
|
18
|
+
## Protocol data
|
|
19
|
+
|
|
20
|
+
`lib/object_framework/http2/hpack_tables.rb` contains the static header table
|
|
21
|
+
and Huffman code table from RFC 7541, Appendices A and B:
|
|
22
|
+
https://www.rfc-editor.org/rfc/rfc7541.html
|
|
23
|
+
|
|
24
|
+
Copyright (c) 2015 IETF Trust and the persons identified as the document authors.
|
|
25
|
+
All rights reserved.
|
|
26
|
+
|
|
27
|
+
Redistribution and use in source and binary forms, with or without
|
|
28
|
+
modification, are permitted provided that the following conditions are met:
|
|
29
|
+
|
|
30
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
31
|
+
this list of conditions and the following disclaimer.
|
|
32
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
33
|
+
this list of conditions and the following disclaimer in the documentation
|
|
34
|
+
and/or other materials provided with the distribution.
|
|
35
|
+
|
|
36
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
37
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
38
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
39
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
40
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
41
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
|
|
42
|
+
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
43
|
+
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
44
|
+
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
|
|
45
|
+
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "object_framework"
|
|
4
|
+
|
|
5
|
+
class Greet
|
|
6
|
+
def initialize(greeting:)
|
|
7
|
+
@greeting = greeting
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def call(name:)
|
|
11
|
+
{ message: "#{@greeting}, #{name}" }
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
app = ObjectFramework::Application.new
|
|
16
|
+
app.factory(:greeting) { "Hello" }
|
|
17
|
+
app.operation(:greet, use_case: Greet, policy: :none, dependencies: [:greeting])
|
|
18
|
+
app.start
|
|
19
|
+
begin
|
|
20
|
+
result = app.invoke(:greet, name: "Ada")
|
|
21
|
+
puts result[:message]
|
|
22
|
+
puts "Result is frozen: #{result.frozen?}"
|
|
23
|
+
ensure
|
|
24
|
+
app.stop
|
|
25
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../orders/domain"
|
|
4
|
+
|
|
5
|
+
order = OrdersExample::Order.new(id: "ready", total_cents: 1500)
|
|
6
|
+
repository = Object.new
|
|
7
|
+
repository.define_singleton_method(:fetch) { |id| order }
|
|
8
|
+
confirm = OrdersExample::ConfirmOrder.new(orders: repository)
|
|
9
|
+
puts confirm.call(order_id: "ready").state
|
|
10
|
+
begin
|
|
11
|
+
confirm.call(order_id: "ready")
|
|
12
|
+
rescue OrdersExample::OrderNotConfirmable
|
|
13
|
+
puts "An order can only be confirmed once"
|
|
14
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "object_framework"
|
|
4
|
+
|
|
5
|
+
class Resource
|
|
6
|
+
def initialize(events:)
|
|
7
|
+
@events = events
|
|
8
|
+
@events << "opened"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def close
|
|
12
|
+
@events << "closed"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class CompareResources
|
|
17
|
+
def initialize(first:, second:)
|
|
18
|
+
@first, @second = first, second
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
@first.equal?(@second)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
events = []
|
|
27
|
+
app = ObjectFramework::Application.new
|
|
28
|
+
app.factory(:resource, close: true) { Resource.new(events:) }
|
|
29
|
+
app.factory(:first, dependencies: [:resource]) { |resource:| resource }
|
|
30
|
+
app.factory(:second, dependencies: [:resource]) { |resource:| resource }
|
|
31
|
+
app.operation(:compare, use_case: CompareResources, policy: :none, dependencies: [:first, :second])
|
|
32
|
+
app.start
|
|
33
|
+
begin
|
|
34
|
+
puts "Same resource: #{app.invoke(:compare)}"
|
|
35
|
+
puts events.join(", ")
|
|
36
|
+
ensure
|
|
37
|
+
app.stop
|
|
38
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "object_framework"
|
|
4
|
+
require_relative "../orders/domain"
|
|
5
|
+
|
|
6
|
+
mappings = ObjectFramework::Persistence::Mappings.new
|
|
7
|
+
mappings.define(OrdersExample::Order, table: :orders) do
|
|
8
|
+
field :id
|
|
9
|
+
field :state
|
|
10
|
+
field :total_cents, type: :integer
|
|
11
|
+
end
|
|
12
|
+
mappings.validate!
|
|
13
|
+
storage = ObjectFramework::Memory::Storage.new
|
|
14
|
+
write = storage.begin_transaction(mode: :write)
|
|
15
|
+
session = ObjectFramework::Persistence::Session.new(transaction: write, mappings:)
|
|
16
|
+
order = OrdersExample::Order.new(total_cents: 1500)
|
|
17
|
+
begin
|
|
18
|
+
session.add(order)
|
|
19
|
+
session.flush
|
|
20
|
+
write.commit
|
|
21
|
+
ensure
|
|
22
|
+
session.close
|
|
23
|
+
write.close
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
read = storage.begin_transaction(mode: :read)
|
|
27
|
+
session = ObjectFramework::Persistence::Session.new(transaction: read, mappings:)
|
|
28
|
+
begin
|
|
29
|
+
first = session.find(OrdersExample::Order, order.id)
|
|
30
|
+
second = session.find(OrdersExample::Order, order.id)
|
|
31
|
+
puts "UUID assigned: #{!order.id.nil?}"
|
|
32
|
+
puts "Same identity: #{first.equal?(second)}"
|
|
33
|
+
puts "Independent session: #{!first.equal?(order)}"
|
|
34
|
+
puts "Stored state: #{first.state}"
|
|
35
|
+
ensure
|
|
36
|
+
session.close
|
|
37
|
+
read.close
|
|
38
|
+
storage.close
|
|
39
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "object_framework"
|
|
4
|
+
|
|
5
|
+
class Customer
|
|
6
|
+
attr_accessor :id, :name
|
|
7
|
+
def initialize(id: nil, name:); @id, @name = id, name; end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class Purchase
|
|
11
|
+
attr_accessor :id, :total_cents, :customer
|
|
12
|
+
def initialize(id: nil, total_cents:)
|
|
13
|
+
@id, @total_cents, @customer = id, total_cents, nil
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
mappings = ObjectFramework::Persistence::Mappings.new
|
|
18
|
+
mappings.define(Customer, table: :customers) { field :id; field :name }
|
|
19
|
+
mappings.define(Purchase, table: :purchases) do
|
|
20
|
+
field :id
|
|
21
|
+
field :total_cents, type: :integer
|
|
22
|
+
belongs_to :customer, target: Customer, foreign_key: :customer_id
|
|
23
|
+
end
|
|
24
|
+
mappings.validate!
|
|
25
|
+
storage = ObjectFramework::Memory::Storage.new
|
|
26
|
+
write = storage.begin_transaction(mode: :write)
|
|
27
|
+
seed = ObjectFramework::Persistence::Session.new(transaction: write, mappings:)
|
|
28
|
+
customer = Customer.new(name: "Ada")
|
|
29
|
+
purchase = Purchase.new(total_cents: 1500)
|
|
30
|
+
purchase.customer = customer
|
|
31
|
+
seed.add(customer)
|
|
32
|
+
seed.add(purchase)
|
|
33
|
+
seed.flush
|
|
34
|
+
write.commit
|
|
35
|
+
seed.close
|
|
36
|
+
write.close
|
|
37
|
+
|
|
38
|
+
read = storage.begin_transaction(mode: :read)
|
|
39
|
+
session = ObjectFramework::Persistence::Session.new(transaction: read, mappings:)
|
|
40
|
+
begin
|
|
41
|
+
query = ObjectFramework::Persistence::Query.new.compare(:total_cents, :gte, 1000).order(:total_cents, :desc).limit(10)
|
|
42
|
+
loaded = session.query(Purchase, query, load: [:customer]).first
|
|
43
|
+
puts "Customer: #{loaded.customer.name}"
|
|
44
|
+
puts "Shared identity: #{loaded.customer.equal?(session.find(Customer, customer.id))}"
|
|
45
|
+
ensure
|
|
46
|
+
session.close
|
|
47
|
+
read.close
|
|
48
|
+
storage.close
|
|
49
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "../orders/application"
|
|
6
|
+
|
|
7
|
+
bundle = OrdersExample.build
|
|
8
|
+
bundle.application.start
|
|
9
|
+
begin
|
|
10
|
+
http = Net::HTTP.new("127.0.0.1", bundle.server.port, nil)
|
|
11
|
+
response = http.post("/orders/ready/confirm", "")
|
|
12
|
+
puts "HTTP #{response.code}"
|
|
13
|
+
puts "Order state: #{JSON.parse(response.body).fetch('state')}"
|
|
14
|
+
ensure
|
|
15
|
+
bundle.application.stop
|
|
16
|
+
bundle.storage.close
|
|
17
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "../orders/application"
|
|
6
|
+
|
|
7
|
+
bundle = OrdersExample.build
|
|
8
|
+
bundle.application.start
|
|
9
|
+
begin
|
|
10
|
+
http = Net::HTTP.new("127.0.0.1", bundle.server.port, nil)
|
|
11
|
+
%w[missing rejected].each do |id|
|
|
12
|
+
response = http.post("/orders/#{id}/confirm", "")
|
|
13
|
+
problem = JSON.parse(response.body)
|
|
14
|
+
puts "#{response.code} #{problem.fetch('code')}"
|
|
15
|
+
puts "Occurrence ID present: #{problem.key?('operation_id')}"
|
|
16
|
+
end
|
|
17
|
+
ensure
|
|
18
|
+
bundle.application.stop
|
|
19
|
+
bundle.storage.close
|
|
20
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require_relative "../orders/application"
|
|
5
|
+
|
|
6
|
+
class GetOrder
|
|
7
|
+
def initialize(session:); @session = session; end
|
|
8
|
+
def call(order_id:)
|
|
9
|
+
order = @session.find(OrdersExample::Order, order_id)
|
|
10
|
+
{ state: order.state }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
Dir.mktmpdir("orders-guide") do |directory|
|
|
15
|
+
schema = ObjectFramework::SQLite::Schema.new(tables: { orders: { id: :string, state: :string, total_cents: :integer } })
|
|
16
|
+
schema.migration(2) { add_column :orders, :note, type: :string }
|
|
17
|
+
storage = ObjectFramework::SQLite::Storage.new(path: File.join(directory, "orders.sqlite3"), schema:)
|
|
18
|
+
bundle = OrdersExample.build(storage:)
|
|
19
|
+
seed = storage.begin_transaction(mode: :write)
|
|
20
|
+
row = seed.find(:orders, "ready")
|
|
21
|
+
seed.update(:orders, "ready", row.merge(note: "gift"))
|
|
22
|
+
seed.commit
|
|
23
|
+
seed.close
|
|
24
|
+
bundle.application.operation(:get_order, use_case: GetOrder, policy: :read, dependencies: [:session])
|
|
25
|
+
bundle.application.start
|
|
26
|
+
begin
|
|
27
|
+
puts "Schema version: #{storage.schema_version}"
|
|
28
|
+
puts "Read: #{bundle.application.invoke(:get_order, order_id: 'ready')[:state]}"
|
|
29
|
+
puts "Write: #{bundle.application.invoke(:confirm_order, order_id: 'ready').state}"
|
|
30
|
+
check = storage.begin_transaction(mode: :read)
|
|
31
|
+
puts "Unmapped note: #{check.find(:orders, 'ready')[:note]}"
|
|
32
|
+
check.close
|
|
33
|
+
puts "Open connections: #{storage.open_connection_count}"
|
|
34
|
+
ensure
|
|
35
|
+
bundle.application.stop
|
|
36
|
+
storage.close
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require_relative "../orders/application"
|
|
6
|
+
require_relative "../orders/contracts"
|
|
7
|
+
|
|
8
|
+
bundle = OrdersExample.build
|
|
9
|
+
service = OrdersExample.grpc_contract
|
|
10
|
+
controller = ObjectFramework::GRPC::Controller.new(application: bundle.application, operation: :confirm_order)
|
|
11
|
+
server = ObjectFramework::GRPC::Server.new(application: bundle.application, services: [service], controllers: { confirm_order: controller })
|
|
12
|
+
bundle.application.install(server)
|
|
13
|
+
bundle.application.start
|
|
14
|
+
begin
|
|
15
|
+
Dir.mktmpdir("orders-grpc-guide") do |directory|
|
|
16
|
+
contract = File.join(directory, "orders.proto")
|
|
17
|
+
File.write(contract, service.to_proto)
|
|
18
|
+
python = ENV.fetch("OBJECT_FRAMEWORK_INTEROP_PYTHON", File.expand_path("../../.venv/bin/python", __dir__))
|
|
19
|
+
output, error, status = Open3.capture3(python, File.expand_path("../orders/grpc_client.py", __dir__), server.port.to_s, contract, "ready")
|
|
20
|
+
raise error unless status.success?
|
|
21
|
+
print output
|
|
22
|
+
end
|
|
23
|
+
ensure
|
|
24
|
+
bundle.application.stop
|
|
25
|
+
bundle.storage.close
|
|
26
|
+
end
|