next_station 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.
data/README.md ADDED
@@ -0,0 +1,790 @@
1
+ # NextStation
2
+
3
+ NextStation is a lightweight, flexible framework for building service objects (Operations) in Ruby. It provides a clean DSL to define business processes, manage state, and handle flow control.
4
+
5
+ ## Index
6
+
7
+ - [Installation](#installation)
8
+ - [Getting Started](#getting-started)
9
+ - [Core Concepts](#core-concepts)
10
+ - [Flow Control](#flow-control)
11
+ - [Railway Pattern & Errors](#railway-pattern--errors)
12
+ - [Input Validation (dry-validation)](#input-validation-dry-validation)
13
+ - [Logging and Monitoring](#logging-and-monitoring)
14
+ - [Dependency Injection](#dependency-injection)
15
+ - [Nested Operations (Operation Composition)](#nested-operations-operation-composition)
16
+ - [Plugin System](#plugin-system)
17
+ - [Advanced Usage](#advanced-usage)
18
+ - [License](#license)
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'next_station'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ $ bundle install
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install next_station
35
+
36
+ ## Getting Started
37
+
38
+ Define an operation by inheriting from `NextStation::Operation` and using the `process` block. You can use `result_at` to specify which key from the state should be returned as the result value:
39
+
40
+ ```ruby
41
+ class CreateUser < NextStation::Operation
42
+ result_at :user_id
43
+
44
+ process do
45
+ step :validate_params
46
+ step :persist_user
47
+ step :send_welcome_email
48
+ end
49
+
50
+ def validate_params(state)
51
+ raise "Invalid email" unless state.params[:email].include?("@")
52
+ state
53
+ end
54
+
55
+ def persist_user(state)
56
+ # state[:params] contains the initial input
57
+ user = User.create(state.params)
58
+ state[:user_id] = user.id
59
+ state
60
+ end
61
+
62
+ def send_welcome_email(state)
63
+ # Logic to send email
64
+ state
65
+ end
66
+ end
67
+
68
+ # Usage
69
+ result = CreateUser.new.call(email: "user@example.com", name: "John Doe")
70
+
71
+ if result.success?
72
+ puts "User created with ID: #{result.value}"
73
+ else
74
+ puts "Error: #{result.error.message}"
75
+ end
76
+ ```
77
+
78
+ ## Core Concepts
79
+
80
+ ### State
81
+
82
+ Every operation execution revolves around a `State` object. It holds:
83
+
84
+ - **params**: The initial input passed to `.call(params, context)`.
85
+ - **context**: Read-only configuration or dependencies (e.g., current_user, repository).
86
+ - **data**: A hash-like storage where steps can read and write data. By default, it contains a reference to `params` under the `:params` key.
87
+
88
+ Steps always receive the `state` as their only argument and MUST return it. If a step returns something else (or `nil`), a `NextStation::StepReturnValueError` will be raised.
89
+
90
+ Inside a step, you can access params in two ways:
91
+ ```ruby
92
+ state.params[:email] # Recommended
93
+ state[:params][:email] # Also valid
94
+ ```
95
+
96
+ Direct access to params via top-level state keys (e.g., `state[:email]`) is NOT supported to avoid confusion between initial input and operation data.
97
+
98
+ ### Result
99
+
100
+ Operations return a `NextStation::Result` object (either a `Success` or `Failure`) which provides:
101
+
102
+ - `success?`: Boolean indicating if the operation finished successfully.
103
+ - `failure?`: Boolean indicating if the operation failed or was halted.
104
+ - `value`: The data returned by the operation (for `Success`).
105
+ - `error`: A `Result::Error` object containing `type`, `message`, `help_url`, and `details`.
106
+
107
+ ## Flow Control
108
+
109
+ NextStation provides powerful tools to manage complex business logic.
110
+
111
+ ### Step Skips
112
+
113
+ You can skip a step conditionally using `skip_if`:
114
+
115
+ ```ruby
116
+ step :send_notification, skip_if: ->(state) { state.params[:do_not_contact] }
117
+ ```
118
+
119
+ ### Branching
120
+
121
+ Use `branch` to execute a group of steps only when a condition is met:
122
+
123
+ ```ruby
124
+ branch ->(state) { state.params[:is_admin] } do
125
+ step :grant_admin_privileges
126
+ step :log_admin_action
127
+ end
128
+ ```
129
+
130
+ Branches can be nested for complex flows.
131
+
132
+ ### Resilience (Retry Logic)
133
+
134
+ Add resilience to flaky steps using `retry_if`, `attempts`, and `delay`:
135
+
136
+ ```ruby
137
+ process do
138
+ step :call_external_api,
139
+ retry_if: ->(state, exception) { exception.is_a?(Timeout::Error) },
140
+ attempts: 3,
141
+ delay: 1
142
+ end
143
+ ```
144
+
145
+ The `retry_if` lambda receives both the current `state` and the `exception` (if any). It should return `true` if the step should be retried.
146
+
147
+ You can also retry based on the state result even if no exception was raised:
148
+
149
+ ```ruby
150
+ step :check_job_status,
151
+ retry_if: ->(state, _exception) { state[:job_status] == "pending" },
152
+ attempts: 5,
153
+ delay: 2
154
+ ```
155
+
156
+ Inside a step, you can check the current attempt number using `state.step_attempt`:
157
+
158
+ ```ruby
159
+ def call_external_api(state)
160
+ puts "Executing attempt number: #{state.step_attempt}"
161
+ # ...
162
+ state
163
+ end
164
+ ```
165
+
166
+ ## Railway Pattern & Errors
167
+
168
+ NextStation supports the Railway pattern, allowing you to explicitly handle success and failure paths using a structured error DSL.
169
+
170
+ ### Defining Errors
171
+
172
+ Use the `errors` block to define possible error types:
173
+
174
+ ```ruby
175
+ class CreateUser < NextStation::Operation
176
+ errors do
177
+ error_type :email_taken do
178
+ message en: "Email %{email} is taken"
179
+ message sp: "El correo %{email} ya existe"
180
+ help_url "http://example.com/support/email-taken"
181
+ end
182
+ end
183
+ end
184
+ ```
185
+
186
+ ### External Errors
187
+
188
+ You can also pass an existing `NextStation::Errors` class to `errors`.
189
+
190
+ ```ruby
191
+
192
+ class MyExternalErrors < NextStation::Errors
193
+ error_type :invalid_token do
194
+ message en: "Invalid token"
195
+ message sp: "Token inválido"
196
+ end
197
+ end
198
+
199
+ class GetUser < NextStation::Operation
200
+ errors MyExternalErrors
201
+ # ...
202
+ end
203
+ ```
204
+
205
+ ### Shared Errors
206
+
207
+ You can define shared error collections by inheriting from `NextStation::Errors`. This allows you to reuse common error
208
+ definitions across multiple operations.
209
+
210
+ ```ruby
211
+
212
+ class MySharedErrors < NextStation::Errors
213
+ error_type :not_found do
214
+ message en: "Resource not found", sp: "Recurso no encontrado"
215
+ end
216
+
217
+ error_type :unauthorized do
218
+ message en: "You are not authorized to perform this action"
219
+ end
220
+ end
221
+
222
+ class GetUser < NextStation::Operation
223
+ # Pass the class directly to errors
224
+ errors MySharedErrors
225
+
226
+ # You can still add operation-specific errors or override shared ones
227
+ errors do
228
+ error_type :user_inactive do
229
+ message en: "User is inactive"
230
+ end
231
+
232
+ error_type :not_found do
233
+ message en: "User with ID %{id} not found"
234
+ end
235
+ end
236
+ end
237
+ ```
238
+
239
+ ### Halting Execution
240
+
241
+ Use `error!` within a step to stop the operation immediately and return a failure result:
242
+
243
+ ```ruby
244
+ def check_email(state)
245
+ if User.exists?(state.params[:email])
246
+ error!(
247
+ type: :email_taken,
248
+ msg_keys: { email: state.params[:email] },
249
+ details: { timestamp: Time.now }
250
+ )
251
+ end
252
+ state
253
+ end
254
+ ```
255
+
256
+ ### Multi-language Support
257
+
258
+ You can specify the desired language when calling the operation via the context:
259
+
260
+ ```ruby
261
+ result = CreateUser.new.call({ email: "taken@example.com" }, { lang: :sp })
262
+ result.error.message # => "El correo taken@example.com ya existe"
263
+ ```
264
+
265
+ If the requested language is not defined, it defaults to `:en`.
266
+
267
+ ## Input Validation (dry-validation)
268
+
269
+ NextStation integrates with `dry-validation` to provide powerful input guarding and coercion.
270
+
271
+ ### Defining a Contract
272
+
273
+ Use `validate_with` to define your validation rules. You can use a block to define the contract inline, or pass an
274
+ existing contract class.
275
+
276
+ #### Inline Contract
277
+
278
+ ```ruby
279
+ class CreateUser < NextStation::Operation
280
+ # Define the contract inline
281
+ validate_with do
282
+ params do
283
+ required(:email).filled(:string, format?: /@/)
284
+ required(:age).filled(:integer, gteq?: 18)
285
+ end
286
+ end
287
+
288
+ process do
289
+ step :validation # Explicitly run the validation
290
+ step :persist
291
+ end
292
+
293
+ def persist(state)
294
+ # state.params now contains COERCED values (e.g., age is an Integer)
295
+ User.create!(state.params)
296
+ state
297
+ end
298
+ end
299
+ ```
300
+
301
+ #### External Contract
302
+
303
+ You can also pass an existing `Dry::Validation::Contract` class.
304
+
305
+ ```ruby
306
+
307
+ class MyExternalContract < Dry::Validation::Contract
308
+ params do
309
+ required(:token).filled(:string)
310
+ end
311
+ end
312
+
313
+ class Authenticate < NextStation::Operation
314
+ validate_with MyExternalContract
315
+
316
+ process do
317
+ step :validation
318
+ step :authorize
319
+ end
320
+
321
+ def authorize(state)
322
+ # state.params[:token] is available here
323
+ state
324
+ end
325
+ end
326
+ ```
327
+
328
+ ### The :validation Step
329
+
330
+ Validation is NOT automatic. You must explicitly add `step :validation` in your `process` block.
331
+
332
+ - **Failure**: If validation fails, the operation halts immediately and returns a `Result::Failure` with type
333
+ `:validation`.
334
+ - **Details**: `result.error.details` contains the raw error hash from `dry-validation`.
335
+ - **Coercion**: On success, `state.params` is updated with the coerced and filtered values from the validation result.
336
+
337
+ ### Customizing Validation Errors
338
+
339
+ You can override the default validation error message using the `errors` DSL:
340
+
341
+ ```ruby
342
+
343
+ class UpdateProfile < NextStation::Operation
344
+ errors do
345
+ error_type :validation do
346
+ message en: "The provided data is invalid: %{errors}",
347
+ sp: "Los datos son inválidos: %{errors}"
348
+ end
349
+ end
350
+
351
+ validate_with do
352
+ # ...
353
+ end
354
+ process { step :validation }
355
+ end
356
+ ```
357
+
358
+ If no custom message is defined, NextStation uses a default message: "One or more parameters are invalid. See validation
359
+ details." (available in English and Spanish).
360
+
361
+ ### Localization
362
+
363
+ NextStation automatically handles localization for validation errors. It defaults to a "slim" approach using the `:yaml` backend, loading translations from its internal configuration.
364
+ For this gem, the locale yml file is located at `lib/next_station/config/errors.yml`.
365
+
366
+ The `lang` passed in the context (e.g., `call(params, { lang: :sp })`) is automatically respected.
367
+
368
+ ```ruby
369
+ class UpdateProfile < NextStation::Operation
370
+ validate_with do
371
+ params do
372
+ required(:name).filled(:string)
373
+ end
374
+ end
375
+
376
+ process { step :validation }
377
+ end
378
+
379
+ # Pass the desired language in the context
380
+ result = UpdateProfile.new.call({ name: "" }, { lang: :sp })
381
+
382
+ # result.error.details will contain the localized messages from dry-validation
383
+ # => { name: ["debe estar lleno"] }
384
+ ```
385
+
386
+ ### Validation Enforcement
387
+
388
+ By default, if you define `validate_with`, the validation is considered enabled.
389
+
390
+ - **force_validation!**: Ensures that `step :validation` is present in the `process` block. If missing, calling the
391
+ operation will raise a `NextStation::ValidationError`.
392
+ - **skip_validation!**: Disables the validation check even if `step :validation` is present.
393
+
394
+ ## Logging and Monitoring
395
+
396
+ NextStation provides a built-in event system powered by `dry-monitor` to track operation lifecycle and user-defined
397
+ logs.
398
+
399
+ ### Bult-in Logging
400
+
401
+ Inside your operation steps, you can use `publish_log` to broadcast custom events. These are automatically routed to the
402
+ configured logger by default.
403
+
404
+ ```ruby
405
+
406
+ class CreateUser < NextStation::Operation
407
+ def persist(state)
408
+ # ... logic ...
409
+ publish_log(:info, "User persisted successfully", user_id: state[:user_id])
410
+ state
411
+ end
412
+ end
413
+ ```
414
+
415
+ - The log will automatically include the fields `trace_id` and `span_id` if the OpenTelemetry SDK is detected,
416
+
417
+ NextStation features an environment-aware logging configuration that works out of the box.
418
+
419
+ - **In Development:** It defaults to the `Console` formatter, providing human-readable, colorized output to `STDOUT`.
420
+ Example:
421
+
422
+ ```Text
423
+ [I][2026-03-01 20:32:54][CreateUser/persist] -- User persisted successfully {:user_id=>1}
424
+ ```
425
+
426
+ - **In Production (or any other environment):** It defaults to the `Json` formatter, which is ideal for structured
427
+ logging. Example:
428
+ ```JSON
429
+ {
430
+ "level": "INFO",
431
+ "time": "2026-03-01T20:32:54.123456",
432
+ "pid": 92323,
433
+ "origin": {
434
+ "operation": "CreateUser",
435
+ "event": "log.custom",
436
+ "step_name": "persist"
437
+ },
438
+ "message": "User persisted successfully",
439
+ "payload": {
440
+ "user_id": 1
441
+ }
442
+ }
443
+ ```
444
+
445
+ ### Configuration
446
+
447
+ You can customize the logger, logging level, and other options:
448
+
449
+ ```ruby
450
+ NextStation.configure do |config|
451
+ # Use a different logger (e.g., Rails.logger)
452
+ config.logger = Rails.logger
453
+
454
+ # Manually override the formatter if needed
455
+ # config.logger.formatter = NextStation::Logging::Formatter::Json.new
456
+
457
+ # Set logging level (:debug, :info, :warn, :error, :fatal, :unknown).
458
+ # :info (default): logs everything except debug level.
459
+ # :warn: logs warn and above levels.
460
+ # :debug: logs everything including individual step start/stop events.
461
+ config.logging_level = :info
462
+
463
+ # To disable default logging subscribers:
464
+ # config.logging_enabled = false
465
+ # config.monitor = MyCustomMonitor.new
466
+ end
467
+ ```
468
+
469
+ ### Lifecycle Events
470
+
471
+ NextStation automatically broadcasts events for every operation and step execution. You can subscribe to these events to
472
+ integrate with external monitoring tools (Datadog, Prometheus, etc.):
473
+
474
+ ```ruby
475
+ NextStation.config.monitor.subscribe("operation.stop") do |event|
476
+ puts "Operation #{event[:operation]} finished in #{event[:duration]}ms"
477
+ end
478
+
479
+ NextStation.config.monitor.subscribe("step.retry") do |event|
480
+ puts "Step #{event[:step]} failed (attempt #{event[:attempt]}) with: #{event[:error].message}"
481
+ end
482
+ ```
483
+
484
+ **Available Events:**
485
+
486
+ - `operation.start`: Triggered when an operation starts.
487
+ - `operation.stop`: Triggered when an operation finishes (success or failure). Includes `duration` and `result`.
488
+ - `step.start`: Triggered before a step starts.
489
+ - `step.stop`: Triggered after a step finishes. Includes `duration` and `state`.
490
+ - `step.retry`: Triggered when a step fails and is about to be retried.
491
+
492
+ ## Dependency Injection
493
+
494
+ NextStation includes a lightweight Dependency Injection (DI) system to help you decouple your operations from their external dependencies.
495
+
496
+ ### Declaring Dependencies
497
+
498
+ Use the `depends` method to declare dependencies and their defaults. Defaults can be static values or lazy lambdas:
499
+
500
+ ```ruby
501
+ class CreateUser < NextStation::Operation
502
+ depends mailer: -> { Mailer.new },
503
+ repository: UserRepository.new
504
+
505
+ process do
506
+ step :send_welcome_email
507
+ end
508
+
509
+ def send_welcome_email(state)
510
+ # Access dependencies using the dependency() method
511
+ dependency(:mailer).send_welcome(state.params[:email])
512
+ state
513
+ end
514
+ end
515
+ ```
516
+
517
+ ### Injecting Dependencies
518
+
519
+ You can override the default dependencies when instantiating the operation by passing the `deps:` keyword argument:
520
+
521
+ ```ruby
522
+ # In your tests
523
+ mock_mailer = double("Mailer")
524
+ operation = CreateUser.new(deps: { mailer: mock_mailer })
525
+ operation.call(email: "test@example.com")
526
+ ```
527
+
528
+ ### Inheritance
529
+
530
+ Dependencies are inherited and can be overridden in subclasses:
531
+
532
+ ```ruby
533
+ class BaseOp < NextStation::Operation
534
+ depends logger: Logger.new
535
+ end
536
+
537
+ class MyOp < BaseOp
538
+ depends logger: CustomLogger.new # Overrides parent dependency
539
+ end
540
+ ```
541
+
542
+ ## Nested Operations (Operation Composition)
543
+
544
+ Operations can invoke other operations using the `call_operation` helper. This maintains the Railway pattern, shares context (e.g., `current_user`, `lang`), and handles error propagation automatically.
545
+
546
+ ```ruby
547
+ class SyncUser < NextStation::Operation
548
+ depends remote_op: -> { RemoteOp.new }
549
+
550
+ errors do
551
+ error_type :provider_error do
552
+ message en: "External Sync Failed: %{reason}"
553
+ end
554
+ end
555
+
556
+ process do
557
+ step :fetch_remote_data
558
+ step :other_step
559
+ end
560
+
561
+ def fetch_remote_data(state)
562
+ # 1. Automatically shares context (state.context)
563
+ # 2. Dynamic params via Proc (or pass a Hash directly)
564
+ # 3. Results stored in state[:remote_profile]
565
+ # 4. If RemoteOp fails with :provider_error, this step halts and
566
+ # the parent returns its own template for :provider_error.
567
+ call_operation(
568
+ state,
569
+ dependency(:remote_op),
570
+ with_params: ->(s) { { uid: s.params[:id] } },
571
+ store_result_in_key: :remote_profile
572
+ )
573
+ end
574
+
575
+ def other_step(state)
576
+ state[:remote_profile] # Access the result from the child operation
577
+ state
578
+ end
579
+ end
580
+ ```
581
+
582
+ ### Error Propagation Rules
583
+
584
+ - **Mapped Error**: If the Parent Operation has a matching `error_type` defined, it "intercepts" the failure. The resulting error uses the Parent's message template but is populated with the Child's `msg_keys` and `details`.
585
+ - **Transparent Error**: If the Parent has NOT defined that error type, the child's `Error` object is propagated exactly as is (including its already resolved message).
586
+
587
+ The `call_operation` helper triggers the internal Halt mechanism, allowing parent step controls like `retry_if` to function as expected.
588
+
589
+ ## Plugin System
590
+
591
+ NextStation features a modular **Plugin System** that allows extending core functionality without modifying the gem
592
+ itself.
593
+
594
+ ### Using Plugins
595
+
596
+ Enable plugins using the `plugin` macro:
597
+
598
+ ```ruby
599
+
600
+ class CreateUser < NextStation::Operation
601
+ plugin :transactional
602
+
603
+ process do
604
+ step :validate_inputs
605
+ transaction do
606
+ step :create_user_record
607
+ end
608
+ end
609
+ end
610
+ ```
611
+
612
+ ### Creating Plugins
613
+
614
+ You can create your own plugins to add lifecycle hooks, DSL methods, and state helpers.
615
+
616
+ For detailed information on how to design and build plugins, please refer to
617
+ the [Plugin System Guide](PLUGIN_SYSTEM_GUIDE.md).
618
+
619
+ ## Advanced Usage
620
+
621
+ ### Result Value and `result_at`
622
+
623
+ Operations return a value encapsulated in the `Result::Success` object. You have two ways to define what this value is:
624
+
625
+ #### 1. Default Result Key (`:result`)
626
+ If you don't specify anything, NextStation looks for the `:result` key in the state.
627
+
628
+ ```ruby
629
+ class MyOperation < NextStation::Operation
630
+ process do
631
+ step :do_work
632
+ end
633
+
634
+ def do_work(state)
635
+ state[:result] = { message: "All good!" }
636
+ state
637
+ end
638
+ end
639
+
640
+ result = MyOperation.new.call
641
+ result.value # => { message: "All good!" }
642
+ ```
643
+
644
+ #### 2. Customizing with `result_at`
645
+ If you want to use a more descriptive key for your result, use `result_at`.
646
+
647
+ ```ruby
648
+ class MyOperation < NextStation::Operation
649
+ result_at :user_record
650
+
651
+ process do
652
+ step :find_user
653
+ end
654
+
655
+ def find_user(state)
656
+ state[:user_record] = User.find(state.params[:id])
657
+ state
658
+ end
659
+ end
660
+
661
+ result = MyOperation.new.call
662
+ result.value # => <User instance>
663
+ ```
664
+
665
+ > **Note:** If the expected key (either `:result` or the one defined by `result_at`) is missing from the state at the end of the operation, a `NextStation::Error` will be raised. This ensures that you explicitly define the output of your operations.
666
+
667
+ ### Output Shapes (dry-struct)
668
+
669
+ You can enforce the structure of the success result using the `result_schema` DSL, which leverages the `dry-struct` gem.
670
+
671
+ ```ruby
672
+ class CreateUser < NextStation::Operation
673
+ result_at :user_data
674
+
675
+ result_schema do
676
+ attribute :id, NextStation::Types::Integer
677
+ attribute :email, NextStation::Types::String
678
+ attribute :address do
679
+ attribute :city, NextStation::Types::String
680
+ attribute :street, NextStation::Types::String
681
+ end
682
+ attribute :metadata, NextStation::Types::Any
683
+ end
684
+
685
+ process do
686
+ step :set_data
687
+ end
688
+
689
+ def set_data(state)
690
+ state[:user_data] = {
691
+ id: 1,
692
+ email: "john@example.com",
693
+ address: { city: "NYC", street: "Main St" },
694
+ metadata: { foo: "bar" }
695
+ }
696
+ state
697
+ end
698
+ end
699
+ ```
700
+
701
+ #### Lazy Validation
702
+
703
+ The result schema is applied **lazily**. Validation and coercion only occur when you call `result.value`.
704
+
705
+ ```ruby
706
+ op = CreateUser.new.call(params)
707
+ op.success? # => true (Operation finished without errors)
708
+
709
+ # Validation happens now:
710
+ op.value
711
+ # => #<CreateUser::ResultSchema id=1 email="john@example.com" ...>
712
+
713
+ # If the data doesn't match the schema:
714
+ # => raises NextStation::ResultShapeError
715
+ ```
716
+
717
+ #### External Schemas
718
+
719
+ You can also pass an existing `Dry::Struct` class to `result_schema`. This is useful for sharing schemas across multiple operations.
720
+
721
+ ```ruby
722
+ class MySharedSchema < Dry::Struct
723
+ attribute :id, NextStation::Types::Integer
724
+ end
725
+
726
+ class CreateUser < NextStation::Operation
727
+ result_schema MySharedSchema
728
+ end
729
+ ```
730
+
731
+ Note that `result_schema` accepts either a `Dry::Struct` class OR a block, but not both. Providing both will raise a `NextStation::DoubleSchemaError`.
732
+
733
+ #### Enabling/Disabling Enforcement
734
+
735
+ By default, enforcement is enabled if a `result_schema` is defined. You can explicitly control this behavior:
736
+
737
+ ```ruby
738
+ class CreateUser < NextStation::Operation
739
+ result_schema do
740
+ # ...
741
+ end
742
+
743
+ # Force enforcement (default if schema is present)
744
+ enforce_result_schema
745
+
746
+ # Disable enforcement (result.value will return the raw hash)
747
+ disable_result_schema
748
+ end
749
+ ```
750
+
751
+ > **Note:** If `enforce_result_schema` is enabled but no `result_schema` is defined (either in the class or its ancestors), calling `result.value` will raise a `NextStation::Error`.
752
+
753
+ #### Types
754
+
755
+ You can use all standard dry-types via `NextStation::Types`.
756
+
757
+ ### Environment Configuration
758
+
759
+ NextStation's behavior can be environment-aware.
760
+
761
+ By default, it automatically detects the environment by checking for `RAILS_ENV`, `RACK_ENV`, `APP_ENV`, and `RUBY_ENV`.
762
+ It considers `development` and `dev` as development environments, and `production`, `prod`, `prd` as production-like.
763
+
764
+ ### Simple Configuration
765
+
766
+ You can set the environment name directly:
767
+
768
+ ```ruby
769
+ NextStation.configure do |config|
770
+ config.environment = 'production'
771
+ # or
772
+ config.environment = ENV['MY_APP_ENV']
773
+ end
774
+ ```
775
+
776
+ ### Advanced Configuration
777
+
778
+ If you need to customize which names are considered "production" or "development", or which environment variables to
779
+ check, you can access the environment object properties:
780
+
781
+ ```ruby
782
+ NextStation.configure do |config|
783
+ # Consider 'staging' as a production-like environment
784
+ config.environment.production_names << 'staging'
785
+ end
786
+ ```
787
+
788
+ ## License
789
+
790
+ TBD