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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/API.md +78 -0
  3. data/README.md +250 -0
  4. data/THIRD_PARTY_NOTICES.md +45 -0
  5. data/examples/guides/01_plain_use_case.rb +13 -0
  6. data/examples/guides/02_composed_application.rb +25 -0
  7. data/examples/guides/03_order_domain.rb +14 -0
  8. data/examples/guides/04_scoped_resources.rb +38 -0
  9. data/examples/guides/05_mapping.rb +39 -0
  10. data/examples/guides/06_associations.rb +49 -0
  11. data/examples/guides/07_http.rb +17 -0
  12. data/examples/guides/08_errors.rb +20 -0
  13. data/examples/guides/09_sqlite.rb +38 -0
  14. data/examples/guides/10_grpc.rb +26 -0
  15. data/examples/guides/11_ractor.rb +15 -0
  16. data/examples/guides/12_box.rb +11 -0
  17. data/examples/guides/13_testing.rb +24 -0
  18. data/examples/guides/manifest.yml +62 -0
  19. data/examples/orders/application.rb +51 -0
  20. data/examples/orders/contracts.rb +19 -0
  21. data/examples/orders/domain.rb +35 -0
  22. data/examples/orders/export_proto.rb +5 -0
  23. data/examples/orders/grpc_client.py +24 -0
  24. data/examples/orders/grpc_server.rb +23 -0
  25. data/examples/orders/server.rb +18 -0
  26. data/lib/object_framework/application.rb +136 -0
  27. data/lib/object_framework/capabilities.rb +59 -0
  28. data/lib/object_framework/errors.rb +85 -0
  29. data/lib/object_framework/execution.rb +184 -0
  30. data/lib/object_framework/grpc/messages.rb +30 -0
  31. data/lib/object_framework/grpc/server.rb +264 -0
  32. data/lib/object_framework/grpc/service.rb +55 -0
  33. data/lib/object_framework/http/io.rb +127 -0
  34. data/lib/object_framework/http/presentation.rb +55 -0
  35. data/lib/object_framework/http/protocol.rb +209 -0
  36. data/lib/object_framework/http/router.rb +80 -0
  37. data/lib/object_framework/http/server.rb +279 -0
  38. data/lib/object_framework/http2/connection.rb +399 -0
  39. data/lib/object_framework/http2/frame.rb +21 -0
  40. data/lib/object_framework/http2/hpack.rb +206 -0
  41. data/lib/object_framework/http2/hpack_tables.rb +333 -0
  42. data/lib/object_framework/http2/inbound.rb +75 -0
  43. data/lib/object_framework/memory/storage.rb +97 -0
  44. data/lib/object_framework/persistence/mapping.rb +150 -0
  45. data/lib/object_framework/persistence/query.rb +97 -0
  46. data/lib/object_framework/persistence/session.rb +173 -0
  47. data/lib/object_framework/persistence/transaction.rb +54 -0
  48. data/lib/object_framework/protobuf.rb +268 -0
  49. data/lib/object_framework/sqlite/compiler.rb +77 -0
  50. data/lib/object_framework/sqlite/schema.rb +95 -0
  51. data/lib/object_framework/sqlite/storage.rb +220 -0
  52. data/lib/object_framework/sqlite.rb +6 -0
  53. data/lib/object_framework/version.rb +5 -0
  54. data/lib/object_framework.rb +31 -0
  55. 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Greet
4
+ def initialize(greeting:)
5
+ @greeting = greeting
6
+ end
7
+
8
+ def call(name:)
9
+ "#{@greeting}, #{name}"
10
+ end
11
+ end
12
+
13
+ puts Greet.new(greeting: "Hello").call(name: "Ada")
@@ -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