diff --git a/CLAUDE.md b/CLAUDE.md index dde5cc6..1b0e470 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,15 +34,19 @@ ``` treaty/ ├── lib/treaty/ +│ ├── action/ # Treaty Action classes +│ │ ├── context/ # Execution context and callable mechanism +│ │ ├── executor/ # Executor wrapper for inventory +│ │ ├── info/rest/ # REST API information generation +│ │ ├── inventory/ # Inventory system +│ │ ├── request/ # Request handling +│ │ ├── response/ # Response handling +│ │ └── versions/ # Version management │ ├── controller/ # Rails controller integration │ ├── entity/ # Entity classes and core attribute system │ │ └── attribute/ # Core attribute system (DSL, validation) │ │ ├── option/ # Option processors (validators, modifiers) │ │ └── validation/ # Attribute validation -│ ├── request/ # Request handling -│ ├── response/ # Response handling -│ ├── versions/ # Version management -│ ├── inventory/ # Inventory system │ └── exceptions/ # Exception classes ├── spec/ │ ├── sandbox/app/ # Example implementations @@ -403,8 +407,8 @@ RSpec.describe Posts::CreateTreaty do subject(:perform) { described_class.call!(context:, inventory:, version:, params:) } let(:context) { instance_double(ApplicationController) } - let(:inventory) { Treaty::Executor::Inventory.new(collection, context) } - let(:collection) { Treaty::Inventory::Collection.new } + let(:inventory) { Treaty::Action::Executor::Inventory.new(collection, context) } + let(:collection) { Treaty::Action::Inventory::Collection.new } context "when valid params" do let(:version) { "1" } diff --git a/docs/api-reference.md b/docs/api-reference.md index cccad40..6943467 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -18,16 +18,16 @@ class MyTreaty < ApplicationTreaty end ``` -## Treaty::Result Class +## Treaty::Action::Result Class ### Overview -Every successful treaty execution returns a `Treaty::Result` object containing the validated data, HTTP status code, and version information. +Every successful treaty execution returns a `Treaty::Action::Result` object containing the validated data, HTTP status code, and version information. **Structure:** ```ruby -class Treaty::Result +class Treaty::Action::Result attr_reader :data, :status, :version end ``` @@ -105,7 +105,7 @@ RSpec.describe Posts::CreateTreaty do let(:params) { { post: { title: "Test" } } } it "returns expected result structure" do - expect(perform).to be_a(Treaty::Result) + expect(perform).to be_a(Treaty::Action::Result) expect(perform.data).to include(:post) expect(perform.status).to eq(201) expect(perform.version).to eq(Gem::Version.new("1")) @@ -150,7 +150,7 @@ end result = Posts::CreateTreaty.call!(version: "1", params: params) result.inspect -# => "#>" +# => "#>" ``` ### Introspection Methods diff --git a/docs/core-concepts.md b/docs/core-concepts.md index b669cdc..153901e 100644 --- a/docs/core-concepts.md +++ b/docs/core-concepts.md @@ -19,7 +19,7 @@ A Treaty (contract) is a formal definition of the data structure for an API endp ### 1. Treaty Class -Inherits from `Treaty::Base` and defines the contract for a specific action. +Inherits from `Treaty::Action::Base` and defines the contract for a specific action. ```ruby class Posts::CreateTreaty < ApplicationTreaty diff --git a/docs/defining-contracts.md b/docs/defining-contracts.md index c603f48..43ced70 100644 --- a/docs/defining-contracts.md +++ b/docs/defining-contracts.md @@ -10,7 +10,7 @@ Learn how to define Treaty contracts, including request and response definitions A Treaty contract consists of: -1. **Class definition** - inheriting from `Treaty::Base` +1. **Class definition** - inheriting from `Treaty::Action::Base` 2. **Version blocks** - one or more version definitions 3. **Request definition** - what data comes in 4. **Response definition(s)** - what data goes out diff --git a/docs/entities.md b/docs/entities.md index ba177cc..cf170f9 100644 --- a/docs/entities.md +++ b/docs/entities.md @@ -658,18 +658,18 @@ end compare_entities(Posts::Create::RequestEntity, Posts::Create::ResponseEntity) ``` -### Comparison with Treaty::Base.info +### Comparison with Treaty::Action::Base.info -While `Treaty::Entity::Base.info` returns entity attribute metadata, `Treaty::Base.info` (for REST API treaties) returns version-based contract information: +While `Treaty::Entity::Base.info` returns entity attribute metadata, `Treaty::Action::Base.info` (for REST API treaties) returns version-based contract information: -| Feature | Treaty::Entity::Base.info | Treaty::Base.info | +| Feature | Treaty::Entity::Base.info | Treaty::Action::Base.info | |---------|---------------------|-------------------| | **Returns** | `Treaty::Info::Entity::Result` | `Treaty::Info::Rest::Result` | | **Primary attribute** | `.attributes` (Hash) | `.versions` (Array) | | **Use case** | Entity structure introspection | API version and contract details | | **Contains** | Attribute types, options, nesting | Versions, executors, request/response specs | -**Example of Treaty::Base.info:** +**Example of Treaty::Action::Base.info:** ```ruby class Posts::IndexTreaty < ApplicationTreaty diff --git a/docs/getting-started.md b/docs/getting-started.md index effb2fb..0b46184 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -46,7 +46,7 @@ end Create `app/treaties/application_treaty.rb`: ```ruby -class ApplicationTreaty < Treaty::Base +class ApplicationTreaty < Treaty::Action::Base end ``` diff --git a/docs/inventory.md b/docs/inventory.md index 73a1d09..2d1d9b6 100644 --- a/docs/inventory.md +++ b/docs/inventory.md @@ -106,7 +106,7 @@ The inventory system uses **lazy evaluation** to optimize performance. Inventory **How it works:** -1. When a treaty executes, a `Treaty::Executor::Inventory` instance is created with: +1. When a treaty executes, a `Treaty::Action::Executor::Inventory` instance is created with: - The inventory collection (definitions from your `provide` calls) - The controller context (for method calls and proc evaluation) @@ -152,7 +152,7 @@ end ## Accessing Inventory in Services -Inventory is passed to services as a `Treaty::Executor::Inventory` instance, which provides both **method-based** and **hash-based** access to inventory items. +Inventory is passed to services as a `Treaty::Action::Executor::Inventory` instance, which provides both **method-based** and **hash-based** access to inventory items. ### Method-Based Access (Recommended) @@ -207,7 +207,7 @@ class Posts::IndexService < ApplicationService::Base end ``` -The `inventory` input receives a `Treaty::Executor::Inventory` instance providing method-based access to evaluated inventory items. +The `inventory` input receives a `Treaty::Action::Executor::Inventory` instance providing method-based access to evaluated inventory items. ### Proc Executors @@ -422,7 +422,7 @@ input :inventory, required: false input :inventory ``` -**Note**: The inventory is a `Treaty::Executor::Inventory` instance. Servactory's type checking is flexible and will accept it. +**Note**: The inventory is a `Treaty::Action::Executor::Inventory` instance. Servactory's type checking is flexible and will accept it. If a Servactory service receives inventory but hasn't declared the input, Servactory will raise `Servactory::Exceptions::Input` error. diff --git a/lib/treaty/action.rb b/lib/treaty/action.rb new file mode 100644 index 0000000..3f2e1c7 --- /dev/null +++ b/lib/treaty/action.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Treaty + # Action namespace containing the base class and versioning system. + # + # Users should inherit from Treaty::Action::Base: + # + # class Posts::CreateTreaty < Treaty::Action::Base + # version 1, default: true do + # summary "Create a new post" + # + # request do + # object :post do + # string :title, :required + # end + # end + # + # response 201 do + # object :post do + # string :id + # end + # end + # + # delegate_to Posts::CreateService + # end + # end + # + # @see Treaty::Action::Base for full documentation and examples + module Action + end +end diff --git a/lib/treaty/action/base.rb b/lib/treaty/action/base.rb new file mode 100644 index 0000000..10d26ed --- /dev/null +++ b/lib/treaty/action/base.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Treaty + module Action + class Base + include Info::DSL + include Context::DSL + include Versions::DSL + end + end +end diff --git a/lib/treaty/action/context/callable.rb b/lib/treaty/action/context/callable.rb new file mode 100644 index 0000000..f5bfcda --- /dev/null +++ b/lib/treaty/action/context/callable.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Context + # Class methods for calling treaty actions. + # + # ## Purpose + # + # Provides the public `call!` class method that serves as the entry + # point for treaty execution. Handles instance creation and delegates + # to instance methods. + # + # ## Usage + # + # Extended into: + # - Treaty classes (via Context::DSL) + # + # Called by: + # - Controller integration + # - Direct treaty invocation in tests + # + # ## Call Pattern + # + # ```ruby + # # Public API: + # result = Posts::CreateTreaty.call!( + # version: "1", + # params: { post: { title: "Hello" } }, + # context: controller, # optional + # inventory: inventory # optional + # ) + # + # result.data # => validated response hash + # result.status # => HTTP status code + # result.version # => resolved version + # ``` + # + # ## Implementation + # + # Creates a new treaty instance and delegates to `_call!` which + # passes through to Workspace, then to Versions::Workspace for + # actual execution. + module Callable + # Executes treaty with given parameters + # + # Main entry point for treaty execution. Creates instance + # and delegates to instance methods for actual work. + # + # @param version [String, nil] Requested API version (nil uses default) + # @param params [Hash] Request parameters + # @param context [Object, nil] Controller context (for inventory evaluation) + # @param inventory [Treaty::Action::Inventory::Collection, nil] Inventory items + # @return [Treaty::Action::Result] Execution result + # @raise [Treaty::Exceptions::VersionNotFound] If version not found + # @raise [Treaty::Exceptions::Validation] If validation fails + # @raise [Treaty::Exceptions::Execution] If service execution fails + def call!(version:, params:, context: nil, inventory: nil) + treaty_instance = send(:new) + + _call!(treaty_instance, context:, inventory:, version:, params:) + end + + private + + # Internal call delegation to instance + # + # Passes all parameters plus class-level collection_of_versions + # to the instance's _call! method. + # + # @param treaty_instance [Object] Treaty instance + # @param context [Object, nil] Controller context + # @param inventory [Treaty::Action::Inventory::Collection, nil] Inventory items + # @param version [String, nil] Requested version + # @param params [Hash] Request parameters + # @return [Treaty::Action::Result] Execution result + def _call!(treaty_instance, context:, inventory:, version:, params:) + treaty_instance.send( + :_call!, + context:, + inventory:, + version:, + params:, + collection_of_versions: + ) + end + end + end + end +end diff --git a/lib/treaty/action/context/dsl.rb b/lib/treaty/action/context/dsl.rb new file mode 100644 index 0000000..34d187d --- /dev/null +++ b/lib/treaty/action/context/dsl.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Context + # DSL module that wires up callable and workspace functionality. + # + # ## Purpose + # + # Acts as a composition root that includes both Callable (class methods) + # and Workspace (instance methods) when included in a treaty class. + # This enables the `call!` API pattern. + # + # ## Usage + # + # Included in: + # - Treaty::Action::Base (as part of core DSL) + # + # ## What it provides + # + # When included, adds: + # - Class method: `call!` (from Callable) + # - Instance methods: `_call!`, `call!` (from Workspace) + # + # ## Architecture + # + # The call chain works as follows: + # + # ``` + # MyTreaty.call!(version:, params:, ...) + # │ + # └─► Callable.call! (class method) + # │ + # ├─► Creates treaty instance + # │ + # └─► Workspace._call! (instance method) + # │ + # └─► Workspace.call! (stores @collection_of_versions) + # │ + # └─► super → Versions::Workspace.call! + # ``` + module DSL + # Hook called when module is included + # + # Extends the including class with Callable (class methods) + # and includes Workspace (instance methods). + # + # @param base [Class] The class including this module + def self.included(base) + base.extend(Callable) + base.include(Workspace) + end + end + end + end +end diff --git a/lib/treaty/action/context/workspace.rb b/lib/treaty/action/context/workspace.rb new file mode 100644 index 0000000..a89594d --- /dev/null +++ b/lib/treaty/action/context/workspace.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Context + # Instance methods for treaty execution context. + # + # ## Purpose + # + # Provides instance-level methods that bridge between Callable + # (class methods) and Versions::Workspace (actual execution). + # Stores the collection_of_versions in instance variable for + # use by Versions::Workspace. + # + # ## Usage + # + # Included via: + # - Context::DSL (when DSL is included) + # + # ## Method Chain + # + # ``` + # Callable.call! (class) + # │ + # └─► _call! (receives all params from class) + # │ + # └─► call! (stores @collection_of_versions) + # │ + # └─► super → Versions::Workspace.call! + # ``` + # + # ## Why Two Methods? + # + # - `_call!` - Entry point from Callable, receives raw parameters + # - `call!` - Stores collection_of_versions, then calls super + # + # The separation allows Versions::Workspace to override `call!` + # while keeping the parameter passing clean. + # + # ## Instance Variable + # + # Stores `@collection_of_versions` which is used by + # Versions::Workspace.call! for version resolution. + module Workspace + private + + # Entry point for instance execution + # + # Receives all parameters from Callable and forwards to call!. + # This method exists to provide a clean interface between + # class methods and instance methods. + # + # @param context [Object, nil] Controller context + # @param inventory [Treaty::Action::Inventory::Collection, nil] Inventory items + # @param version [String, nil] Requested version + # @param params [Hash] Request parameters + # @param collection_of_versions [Treaty::Action::Versions::Collection] Version factories + # @return [Treaty::Action::Result] Execution result + def _call!( + context:, + inventory:, + version:, + params:, + collection_of_versions: + ) + call!( + context:, + inventory:, + version:, + params:, + collection_of_versions: + ) + end + + # Stores collection and delegates to Versions::Workspace + # + # Captures collection_of_versions in instance variable, + # then calls super which invokes Versions::Workspace.call! + # for actual treaty execution. + # + # @param collection_of_versions [Treaty::Action::Versions::Collection] Version factories + # @return [Treaty::Action::Result] Execution result (from super) + def call!( + collection_of_versions:, + ** + ) + @collection_of_versions = collection_of_versions + end + end + end + end +end diff --git a/lib/treaty/action/executor/inventory.rb b/lib/treaty/action/executor/inventory.rb new file mode 100644 index 0000000..62f82c6 --- /dev/null +++ b/lib/treaty/action/executor/inventory.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Executor + # Lazy-evaluating proxy for accessing inventory items. + # + # ## Purpose + # + # Provides lazy evaluation and caching of inventory items defined in controllers. + # Acts as a proxy that evaluates inventory items only when accessed, avoiding + # unnecessary computation for unused items. + # + # ## Usage + # + # Created internally by: + # - Versions::Execution::Request (when executing treaty versions) + # + # Passed to: + # - Service classes (as `inventory` parameter) + # - Proc executors (as `inventory:` keyword argument) + # + # ## Access Patterns + # + # Method-based access (lazy, cached): + # inventory.current_user # Evaluates and caches on first call + # inventory.current_user # Returns cached value + # + # Hash-based access (evaluates all items): + # inventory.to_h # => { current_user: , posts: [...] } + # + # ## Caching + # + # Once an item is evaluated via method access, the result is cached + # in `@evaluated_cache`. Subsequent calls return the cached value + # without re-evaluation. + # + # ## Example + # + # # In controller: + # treaty :index do + # provide :current_user + # provide :posts, from: :load_posts + # end + # + # # In service: + # class Posts::IndexService + # def call(inventory:, params:) + # user = inventory.current_user # Lazy evaluation + # posts = inventory.posts # Lazy evaluation + # # ... + # end + # end + class Inventory + # Creates a new inventory executor instance + # + # @param inventory [Treaty::Action::Inventory::Collection, nil] Collection of inventory items + # @param context [Object] Controller context for evaluating items (typically ActionController instance) + def initialize(inventory, context) + @inventory = inventory + @context = context + @evaluated_cache = {} + end + + # Handles dynamic method calls to access inventory items + # + # Looks up the inventory item by method name, evaluates it with + # the controller context, and caches the result. + # + # @param method_name [Symbol] Name of the inventory item to access + # @return [Object] Evaluated value from the inventory item + # @raise [Treaty::Exceptions::Inventory] If item with given name not found + def method_missing(method_name, *_args) + return @evaluated_cache[method_name] if @evaluated_cache.key?(method_name) + + item = find_inventory_item(method_name) + + @evaluated_cache[method_name] = item.evaluate(@context) + end + + # Checks if method corresponds to an inventory item + # + # @param method_name [Symbol] Method name to check + # @param include_private [Boolean] Whether to include private methods + # @return [Boolean] True if inventory contains item with given name + def respond_to_missing?(method_name, include_private = false) + return false if @inventory.nil? + + @inventory.names.include?(method_name) || super + end + + # Evaluates all inventory items and returns as hash + # + # Unlike method-based access, this evaluates ALL items at once. + # Useful when you need all inventory data as a hash. + # + # @return [Hash{Symbol => Object}] Hash of all evaluated inventory values + def to_h + return {} if @inventory.nil? + + @inventory.evaluate(@context) + end + + # Returns human-readable representation for debugging + # + # @return [String] Inspection string with available item names + def inspect + items = @inventory&.names || [] + "#" + end + + private + + # Finds inventory item by name or raises error + # + # @param name [Symbol] Name of the inventory item + # @return [Treaty::Action::Inventory::Inventory] Found inventory item + # @raise [Treaty::Exceptions::Inventory] If item not found + def find_inventory_item(name) + item = @inventory&.find { |item| item.name == name } + + return item if item + + available = @inventory&.names || [] + + raise Treaty::Exceptions::Inventory, + I18n.t( + "treaty.executor.inventory.item_not_found", + name:, + available: available.join(", ") + ) + end + end + end + end +end diff --git a/lib/treaty/info/rest/builder.rb b/lib/treaty/action/info/builder.rb similarity index 99% rename from lib/treaty/info/rest/builder.rb rename to lib/treaty/action/info/builder.rb index 25def52..39f5d8f 100644 --- a/lib/treaty/info/rest/builder.rb +++ b/lib/treaty/action/info/builder.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module Treaty - module Info - module Rest + module Action + module Info class Builder attr_reader :versions diff --git a/lib/treaty/info/rest/dsl.rb b/lib/treaty/action/info/dsl.rb similarity index 93% rename from lib/treaty/info/rest/dsl.rb rename to lib/treaty/action/info/dsl.rb index 207188c..90dd296 100644 --- a/lib/treaty/info/rest/dsl.rb +++ b/lib/treaty/action/info/dsl.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module Treaty - module Info - module Rest + module Action + module Info module DSL def self.included(base) base.extend(ClassMethods) diff --git a/lib/treaty/info/rest/result.rb b/lib/treaty/action/info/result.rb similarity index 86% rename from lib/treaty/info/rest/result.rb rename to lib/treaty/action/info/result.rb index d27d4fc..5f72150 100644 --- a/lib/treaty/info/rest/result.rb +++ b/lib/treaty/action/info/result.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module Treaty - module Info - module Rest + module Action + module Info class Result attr_reader :versions diff --git a/lib/treaty/action/inventory/collection.rb b/lib/treaty/action/inventory/collection.rb new file mode 100644 index 0000000..af44eab --- /dev/null +++ b/lib/treaty/action/inventory/collection.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Inventory + # Collection wrapper for sets of inventory items. + # + # ## Purpose + # + # Provides a unified interface for working with collections of inventory items. + # Uses Ruby Set internally for uniqueness but exposes Array-like interface. + # + # ## Usage + # + # Used internally by: + # - Inventory::Factory (to store inventory items) + # - Executor::Inventory (to evaluate and access items) + # + # ## Methods + # + # Delegates common collection methods to internal Set: + # - `<<` - Add inventory item + # - `each_with_object` - Iteration with accumulator + # - `find` - Access by condition + # - `empty?` - Size check + # + # Custom methods: + # - `exists?` - Returns true if collection is not empty + # - `names` - Returns array of inventory item names + # - `evaluate` - Evaluates all items with controller context + # + # ## Example + # + # collection = Collection.new + # collection << Inventory.new(name: :current_user, source: :current_user) + # collection << Inventory.new(name: :posts, source: :load_posts) + # collection.exists? # => true + # collection.names # => [:current_user, :posts] + class Collection + extend Forwardable + + def_delegators :@collection, :<<, :each_with_object, :find, :empty? + + # Creates a new collection instance + # + # @param collection [Set] Initial collection (default: empty Set) + def initialize(collection = Set.new) + @collection = collection + end + + # Checks if collection has any elements + # + # @return [Boolean] True if collection is not empty + def exists? + !empty? + end + + # Returns array of all inventory item names + # + # @return [Array] Array of inventory item names + def names + @collection.each_with_object([]) { |item, names| names << item.name } + end + + # Evaluates all inventory items with controller context + # + # @param context [Object] Controller context for evaluation + # @return [Hash{Symbol => Object}] Hash of evaluated inventory values + def evaluate(context) + @collection.each_with_object({}) do |inventory_item, hash| + hash[inventory_item.name] = inventory_item.evaluate(context) + end + end + end + end + end +end diff --git a/lib/treaty/action/inventory/factory.rb b/lib/treaty/action/inventory/factory.rb new file mode 100644 index 0000000..a8877ca --- /dev/null +++ b/lib/treaty/action/inventory/factory.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Inventory + # Factory for building inventory collections from controller DSL. + # + # ## Purpose + # + # Provides the `provide` DSL method used in controller treaty blocks + # to define inventory items. Inventory allows controllers to pass + # data (current_user, loaded records, etc.) to services. + # + # ## Usage + # + # Created by: + # - Controller::DSL (when treaty block is evaluated) + # + # ## DSL Method + # + # The only supported method is `provide`: + # + # treaty :index do + # provide :current_user # Shorthand: same name as method + # provide :posts, from: :load_posts # Symbol source + # provide :meta, from: -> { build_meta } # Proc source + # provide :limit, from: 10 # Direct value + # end + # + # ## Source Types + # + # | Type | Description | Evaluation | + # |------|-------------|------------| + # | Symbol | Controller method name | `context.send(source)` | + # | Proc | Lambda/block | `context.instance_exec(&source)` | + # | Other | Direct value | Returned as-is | + # + # ## Example + # + # factory = Factory.new(:index) + # factory.provide :current_user + # factory.provide :posts, from: :load_posts + # factory.collection # => Collection with 2 items + class Factory + # @return [Treaty::Action::Inventory::Collection] Collection of inventory items + attr_reader :collection + + # Creates a new factory instance + # + # @param action_name [Symbol] Controller action name (for error messages) + def initialize(action_name) + @action_name = action_name + @collection = Collection.new + end + + # Handles DSL method calls (only `provide` is supported) + # + # Creates an Inventory item and adds it to the collection. + # Validates that only `provide` method is called and name is a Symbol. + # + # @param method_name [Symbol] Method name (must be :provide) + # @param args [Array] Arguments (first must be inventory name as Symbol) + # @param options [Hash] Options (:from for source) + # @raise [Treaty::Exceptions::Inventory] If method is not `provide` + # @raise [Treaty::Exceptions::Inventory] If name is not a Symbol + # @return [Treaty::Action::Inventory::Collection] Updated collection + def method_missing(method_name, *args, **options, &_block) # rubocop:disable Metrics/MethodLength + unless method_name == :provide + raise Treaty::Exceptions::Inventory, + I18n.t( + "treaty.inventory.unknown_method", + method: method_name, + action: @action_name + ) + end + + inventory_name = args.first + + unless inventory_name.is_a?(Symbol) + raise Treaty::Exceptions::Inventory, + I18n.t( + "treaty.inventory.name_must_be_symbol", + name: inventory_name.inspect + ) + end + + source = if options.key?(:from) + options.fetch(:from) + else + inventory_name + end + + @collection << Inventory.new(name: inventory_name, source:) + + @collection + end + + # Checks if method should be handled by method_missing + # + # @param method_name [Symbol] Method name + # @return [Boolean] True only for :provide + def respond_to_missing?(method_name, *) + method_name == :provide || super + end + end + end + end +end diff --git a/lib/treaty/action/inventory/inventory.rb b/lib/treaty/action/inventory/inventory.rb new file mode 100644 index 0000000..ea082a0 --- /dev/null +++ b/lib/treaty/action/inventory/inventory.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Inventory + # Individual inventory item with lazy evaluation. + # + # ## Purpose + # + # Represents a single piece of data that can be passed from controller + # to service. Stores the name and source, and can be evaluated against + # a controller context when needed. + # + # ## Usage + # + # Created by: + # - Inventory::Factory (when `provide` is called) + # + # Consumed by: + # - Inventory::Collection (for bulk evaluation) + # - Executor::Inventory (for lazy single-item evaluation) + # + # ## Source Types + # + # | Type | Example | Evaluation | + # |------|---------|------------| + # | Symbol | `:current_user` | Calls `context.send(:current_user)` | + # | Proc | `-> { Time.current }` | Calls `context.instance_exec(&proc)` | + # | Other | `10`, `"string"` | Returns value as-is | + # + # ## Lazy Evaluation + # + # The source is NOT evaluated at creation time. Evaluation happens + # only when `evaluate(context)` is called, typically during treaty + # execution when the service needs the value. + # + # ## Example + # + # # Symbol source (method call) + # item = Inventory.new(name: :current_user, source: :current_user) + # item.evaluate(controller) # => calls controller.current_user + # + # # Proc source (block execution) + # item = Inventory.new(name: :meta, source: -> { { time: Time.current } }) + # item.evaluate(controller) # => executes block in controller context + # + # # Direct value + # item = Inventory.new(name: :limit, source: 10) + # item.evaluate(controller) # => 10 + class Inventory + # @return [Symbol] Inventory item name + attr_reader :name + + # @return [Symbol, Proc, Object] Source for evaluation + attr_reader :source + + # Creates a new inventory item + # + # @param name [Symbol] Item name (must be non-empty Symbol) + # @param source [Symbol, Proc, Object] Evaluation source + # @raise [Treaty::Exceptions::Inventory] If name is invalid + # @raise [Treaty::Exceptions::Inventory] If source is nil + def initialize(name:, source:) + validate_name!(name) + validate_source!(source) + + @name = name + @source = source + end + + # Evaluates source against controller context + # + # Behavior depends on source type: + # - Symbol: calls method on context + # - Proc: executes in context scope + # - Other: returns value directly + # + # @param context [Object] Controller instance + # @return [Object] Evaluated value + # @raise [Treaty::Exceptions::Inventory] If evaluation fails + def evaluate(context) # rubocop:disable Metrics/MethodLength + case source + when Symbol + evaluate_symbol(context) + when Proc + evaluate_proc(context) + else + source + end + rescue StandardError => e + raise Treaty::Exceptions::Inventory, + I18n.t( + "treaty.inventory.evaluation_error", + name: @name, + error: e.message + ) + end + + private + + # Evaluates Symbol source by calling method on context + # + # @param context [Object] Controller instance + # @return [Object] Method return value + def evaluate_symbol(context) + context.send(source) + end + + # Evaluates Proc source in context scope + # + # Uses instance_exec so proc has access to controller + # instance variables and private methods. + # + # @param context [Object] Controller instance + # @return [Object] Proc return value + def evaluate_proc(context) + context.instance_exec(&source) + end + + # Validates that name is a non-empty Symbol + # + # @param name [Object] Name to validate + # @raise [Treaty::Exceptions::Inventory] If invalid + # @return [void] + def validate_name!(name) + return if name.is_a?(Symbol) && !name.to_s.empty? + + raise Treaty::Exceptions::Inventory, + I18n.t("treaty.inventory.invalid_name", name: name.inspect) + end + + # Validates that source is not nil + # + # @param source [Object] Source to validate + # @raise [Treaty::Exceptions::Inventory] If nil + # @return [void] + def validate_source!(source) + return unless source.nil? + + raise Treaty::Exceptions::Inventory, + I18n.t("treaty.inventory.source_required") + end + end + end + end +end diff --git a/lib/treaty/action/request/attribute/attribute.rb b/lib/treaty/action/request/attribute/attribute.rb new file mode 100644 index 0000000..8c65dae --- /dev/null +++ b/lib/treaty/action/request/attribute/attribute.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Request + module Attribute + # Request-specific attribute definition. + # + # ## Purpose + # + # Extends Entity::Attribute::Base with Request-specific behavior. + # Key difference: attributes are **required by default**. + # + # ## Default Behavior + # + # Unlike Response attributes (optional by default), Request attributes + # default to `required: true`. This enforces strict input validation. + # + # ## Usage + # + # Created internally by: + # - Request::Attribute::Builder (when defining nested attributes) + # - Request::Entity (when defining top-level attributes) + # + # ## Nesting + # + # Object and array types create nested builders: + # + # request do + # object :post do # Creates Attribute with nested builder + # string :title # Nested attribute (required by default) + # array :tags do # Nested array + # string :_self # Array element definition + # end + # end + # end + # + # ## Example + # + # # These are equivalent: + # string :title # required by default + # string :title, :required # explicit required + # + # # Must explicitly mark optional: + # string :bio, :optional + class Attribute < Treaty::Entity::Attribute::Base + private + + # Sets default required behavior for request attributes + # + # Request attributes are required by default (is: true). + # This can be overridden with `:optional` helper or `required: false`. + # + # @return [void] + def apply_defaults! + @options[:required] ||= { is: true, message: nil } + end + + # Creates nested builder for object/array type processing + # + # When a block is given to object or array attributes, + # creates a Builder to process the nested attribute definitions. + # + # @param block [Proc] Block containing nested attribute definitions + # @return [void] + def process_nested_attributes(&block) + return unless object_or_array? + + builder = Builder.new(collection_of_attributes, @nesting_level + 1) + builder.instance_eval(&block) + end + end + end + end + end +end diff --git a/lib/treaty/action/request/attribute/builder.rb b/lib/treaty/action/request/attribute/builder.rb new file mode 100644 index 0000000..5f29507 --- /dev/null +++ b/lib/treaty/action/request/attribute/builder.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Request + module Attribute + # DSL builder for defining request attributes. + # + # ## Purpose + # + # Provides Request-specific implementation of the attribute builder. + # Creates Request::Attribute::Attribute instances instead of generic ones. + # + # ## Inheritance + # + # Extends Treaty::Entity::Attribute::Builder::Base which provides: + # - DSL interface (string, integer, object, array, etc.) + # - method_missing magic for type-based method calls + # - Helper support (:required, :optional) + # - Entity reuse via use_entity + # + # ## Usage + # + # Used internally by: + # - Request::Attribute::Attribute (when processing nested object/array blocks) + # + # ## DSL Example + # + # request do + # object :post do + # string :title, :required + # string :content + # array :tags do + # string :_self + # end + # end + # end + # + # ## Methods + # + # Implements abstract methods from base class: + # - `create_attribute` - Creates Request::Attribute::Attribute + # - `deep_copy_attribute` - Deep copies attribute for use_entity support + class Builder < Treaty::Entity::Attribute::Builder::Base + private + + # Creates a new request attribute instance + # + # Called by base class when defining attributes via DSL. + # + # @param name [Symbol] Attribute name + # @param type [Symbol] Attribute type (:string, :integer, :object, etc.) + # @param helpers [Array] Helper symbols (:required, :optional) + # @param nesting_level [Integer] Current nesting depth + # @param options [Hash] Attribute options (default:, format:, etc.) + # @param block [Proc] Block for nested attributes (object/array) + # @return [Treaty::Action::Request::Attribute::Attribute] Created attribute instance + def create_attribute(name, type, *helpers, nesting_level:, **options, &block) + Attribute.new( + name, + type, + *helpers, + nesting_level:, + **options, + &block + ) + end + + # Deep copies an attribute with adjusted nesting level + # + # Used when copying attributes from Entity classes via use_entity. + # Recursively copies nested attributes for object/array types. + # + # @param source_attribute [Treaty::Entity::Attribute::Base] Source attribute to copy + # @param new_nesting_level [Integer] Nesting level for copied attribute + # @return [Treaty::Action::Request::Attribute::Attribute] Deep copied attribute + def deep_copy_attribute(source_attribute, new_nesting_level) # rubocop:disable Metrics/MethodLength + copied = Attribute.new( + source_attribute.name, + source_attribute.type, + nesting_level: new_nesting_level, + **deep_copy_options(source_attribute.options) + ) + + return copied unless source_attribute.nested? + + source_attribute.collection_of_attributes.each do |nested_source| + nested_copied = deep_copy_attribute(nested_source, new_nesting_level + 1) + copied.collection_of_attributes << nested_copied + end + + copied + end + end + end + end + end +end diff --git a/lib/treaty/action/request/entity.rb b/lib/treaty/action/request/entity.rb new file mode 100644 index 0000000..4f98b8a --- /dev/null +++ b/lib/treaty/action/request/entity.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Request + # Internal entity class for request attribute definitions. + # + # ## Purpose + # + # Provides DSL interface for defining request attributes when using + # inline block syntax in treaty definitions. Serves as the anonymous + # class base when `request do ... end` is used. + # + # ## Usage + # + # Created internally by: + # - Request::Factory (when using inline DSL blocks) + # + # ## DSL Interface + # + # Includes Treaty::Entity::Attribute::DSL which provides: + # - Type methods: string, integer, boolean, date, time, datetime + # - Structure methods: object, array + # - Helper support: :required, :optional + # + # ## Difference from Treaty::Entity::Base + # + # While Treaty::Entity::Base creates standalone entity classes, + # Request::Entity creates Request-specific attributes with: + # - Required by default behavior + # - Request::Attribute::Attribute instances + # + # ## Example + # + # # When you write: + # version 1 do + # request do + # object :post do + # string :title, :required + # end + # end + # end + # + # # Factory creates: Class.new(Request::Entity) + # # and calls string, object etc. on it + class Entity + include Treaty::Entity::Attribute::DSL + + class << self + private + + # Creates request-specific attribute instances + # + # Called by DSL methods (string, integer, etc.) to create + # Request::Attribute::Attribute instead of generic attributes. + # + # @param name [Symbol] Attribute name + # @param type [Symbol] Attribute type + # @param helpers [Array] Helper symbols (:required, :optional) + # @param nesting_level [Integer] Current nesting depth + # @param options [Hash] Attribute options + # @param block [Proc] Block for nested attributes + # @return [Treaty::Action::Request::Attribute::Attribute] Created attribute + def create_attribute(name, type, *helpers, nesting_level:, **options, &block) + Attribute::Attribute.new( + name, + type, + *helpers, + nesting_level:, + **options, + &block + ) + end + end + end + end + end +end diff --git a/lib/treaty/action/request/factory.rb b/lib/treaty/action/request/factory.rb new file mode 100644 index 0000000..e5c21f6 --- /dev/null +++ b/lib/treaty/action/request/factory.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Request + # Factory for creating request attribute collections. + # + # ## Purpose + # + # Captures request attribute definitions from treaty DSL and provides + # access to the resulting attribute collection. Supports both inline + # block syntax and Entity class references. + # + # ## Usage + # + # Created internally by: + # - Versions::Factory (when `request do ... end` is called) + # + # Consumed by: + # - Request::Validator (to validate incoming params) + # - Info::Builder (to build request schema information) + # + # ## Definition Modes + # + # ### Inline Block Mode + # + # request do + # object :post do + # string :title, :required + # end + # end + # + # ### Entity Class Mode + # + # request Posts::Create::RequestEntity + # + # ## Implementation + # + # Uses method_missing to forward DSL calls to a dynamically created + # Entity class. The Entity class collects all attribute definitions. + # + # ## Example + # + # factory = Request::Factory.new + # factory.object :post do + # factory.string :title, :required + # end + # factory.collection_of_attributes # => Collection with post attribute + class Factory + # Registers an Entity class for request schema + # + # Use this to reference a pre-defined Entity class instead of + # inline attribute definitions. + # + # @param entity_class [Class] Must be Treaty::Entity::Base subclass + # @raise [Treaty::Exceptions::Validation] If entity_class is invalid + # @return [void] + def use_entity(entity_class) + validate_entity_class!(entity_class) + @entity_class = entity_class + end + + # Returns the collection of defined attributes + # + # @return [Treaty::Entity::Attribute::Collection] Attribute collection + def collection_of_attributes + return Treaty::Entity::Attribute::Collection.new if @entity_class.nil? + + @entity_class.collection_of_attributes + end + + # Forwards DSL method calls to internal Entity class + # + # Creates an anonymous Entity class on first call, then forwards + # all DSL methods (string, integer, object, etc.) to it. + # + # @param type [Symbol] Attribute type (method name) + # @param helpers [Array] Helper symbols and arguments + # @param options [Hash] Attribute options + # @param block [Proc] Block for nested attributes + # @return [void] + def method_missing(type, *helpers, **options, &block) + @entity_class ||= Class.new(Entity) + + @entity_class.public_send(type, *helpers, **options, &block) + end + + # Checks if method should be handled by method_missing + # + # @param name [Symbol] Method name + # @return [Boolean] + def respond_to_missing?(name, *) + super + end + + private + + # Validates that entity_class is a Treaty::Entity::Base subclass + # + # @param entity_class [Class] Class to validate + # @raise [Treaty::Exceptions::Validation] If validation fails + # @return [void] + def validate_entity_class!(entity_class) + return if entity_class.is_a?(Class) && entity_class < Treaty::Entity::Base + + raise Treaty::Exceptions::Validation, + I18n.t( + "treaty.request.factory.invalid_entity_class", + type: entity_class.class, + value: entity_class + ) + end + end + end + end +end diff --git a/lib/treaty/action/request/validator.rb b/lib/treaty/action/request/validator.rb new file mode 100644 index 0000000..1b16b86 --- /dev/null +++ b/lib/treaty/action/request/validator.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Request + # Validates incoming request parameters against schema. + # + # ## Purpose + # + # Validates incoming request params against the request schema defined + # in the treaty version. Runs all validators (required, type, inclusion, + # format) and transformations (default, cast, transform, as). + # + # ## Usage + # + # Called internally by: + # - Versions::Execution::Base (before delegating to service) + # + # ## Validation Flow + # + # 1. Convert ActionController::Parameters to hash if needed + # 2. Check if request schema is defined + # 3. Create dynamic Orchestrator with version's attributes + # 4. Run validation pipeline (validate + transform) + # 5. Return validated/transformed params or raise error + # + # ## Error Handling + # + # Raises Treaty::Exceptions::Validation with detailed messages + # including attribute path and specific validation failures. + # + # ## Example + # + # # Typically called via class method: + # validated = Request::Validator.validate!( + # params: controller.params, + # version_factory: version_factory + # ) + # + # # validated contains transformed params ready for service + class Validator + class << self + # Validates request parameters + # + # @param params [Hash, ActionController::Parameters] Request params + # @param version_factory [Treaty::Action::Versions::Factory] Version with request schema + # @return [Hash] Validated and transformed parameters + # @raise [Treaty::Exceptions::Validation] If validation fails + def validate!(params:, version_factory:) + new(params:, version_factory:).validate! + end + end + + # Creates new validator instance + # + # @param params [Hash, ActionController::Parameters] Request params + # @param version_factory [Treaty::Action::Versions::Factory] Version with request schema + def initialize(params:, version_factory:) + @params = params + @version_factory = version_factory + end + + # Runs validation pipeline + # + # @return [Hash] Validated and transformed parameters + # @raise [Treaty::Exceptions::Validation] If validation fails + def validate! + validate_request_attributes! + end + + private + + # Converts params to plain hash + # + # Handles both ActionController::Parameters (with to_unsafe_h) + # and plain Hash objects. + # + # @return [Hash] Plain hash of request data + def request_data + @request_data ||= begin + @params.to_unsafe_h + rescue NoMethodError + @params + end + end + + # Validates and transforms request data + # + # Creates dynamic Orchestrator class that uses version's request + # attributes for validation. Returns raw data if no schema defined. + # + # @return [Hash] Validated and transformed data + # @raise [Treaty::Exceptions::Validation] If validation fails + def validate_request_attributes! + return request_data unless request_attributes_exist? + + orchestrator_class = Class.new(Treaty::Entity::Attribute::Validation::Orchestrator::Base) do + define_method(:collection_of_attributes) do + @version_factory.request_factory.collection_of_attributes + end + end + + orchestrator_class.validate!( + version_factory: @version_factory, + data: request_data + ) + end + + # Checks if request schema is defined for this version + # + # @return [Boolean] True if request attributes exist + def request_attributes_exist? + return false if @version_factory.request_factory&.collection_of_attributes&.empty? + + @version_factory.request_factory.collection_of_attributes.exists? + end + end + end + end +end diff --git a/lib/treaty/action/response/attribute/attribute.rb b/lib/treaty/action/response/attribute/attribute.rb new file mode 100644 index 0000000..a4c4b7d --- /dev/null +++ b/lib/treaty/action/response/attribute/attribute.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Response + module Attribute + # Response-specific attribute definition. + # + # ## Purpose + # + # Extends Entity::Attribute::Base with Response-specific behavior. + # Key difference: attributes are **optional by default**. + # + # ## Default Behavior + # + # Unlike Request attributes (required by default), Response attributes + # default to `required: false`. This allows flexible response structures + # where not all fields must be present. + # + # ## Usage + # + # Created internally by: + # - Response::Attribute::Builder (when defining nested attributes) + # - Response::Entity (when defining top-level attributes) + # + # ## Nesting + # + # Object and array types create nested builders: + # + # response 200 do + # object :post do # Creates Attribute with nested builder + # string :title # Nested attribute (optional by default) + # array :comments do # Nested array + # object :_self do # Array element definition + # string :text + # end + # end + # end + # end + # + # ## Example + # + # # These are equivalent: + # string :title # optional by default + # string :title, :optional # explicit optional + # + # # Must explicitly mark required: + # string :id, :required + class Attribute < Treaty::Entity::Attribute::Base + private + + # Sets default required behavior for response attributes + # + # Response attributes are optional by default (is: false). + # This can be overridden with `:required` helper or `required: true`. + # + # @return [void] + def apply_defaults! + @options[:required] ||= { is: false, message: nil } + end + + # Creates nested builder for object/array type processing + # + # When a block is given to object or array attributes, + # creates a Builder to process the nested attribute definitions. + # + # @param block [Proc] Block containing nested attribute definitions + # @return [void] + def process_nested_attributes(&block) + return unless object_or_array? + + builder = Builder.new(collection_of_attributes, @nesting_level + 1) + builder.instance_eval(&block) + end + end + end + end + end +end diff --git a/lib/treaty/action/response/attribute/builder.rb b/lib/treaty/action/response/attribute/builder.rb new file mode 100644 index 0000000..ee80350 --- /dev/null +++ b/lib/treaty/action/response/attribute/builder.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Response + module Attribute + # DSL builder for defining response attributes. + # + # ## Purpose + # + # Provides Response-specific implementation of the attribute builder. + # Creates Response::Attribute::Attribute instances instead of generic ones. + # + # ## Inheritance + # + # Extends Treaty::Entity::Attribute::Builder::Base which provides: + # - DSL interface (string, integer, object, array, etc.) + # - method_missing magic for type-based method calls + # - Helper support (:required, :optional) + # - Entity reuse via use_entity + # + # ## Usage + # + # Used internally by: + # - Response::Attribute::Attribute (when processing nested object/array blocks) + # + # ## DSL Example + # + # response 201 do + # object :post do + # string :id + # string :title + # datetime :created_at + # end + # end + # + # ## Methods + # + # Implements abstract methods from base class: + # - `create_attribute` - Creates Response::Attribute::Attribute + # - `deep_copy_attribute` - Deep copies attribute for use_entity support + class Builder < Treaty::Entity::Attribute::Builder::Base + private + + # Creates a new response attribute instance + # + # Called by base class when defining attributes via DSL. + # + # @param name [Symbol] Attribute name + # @param type [Symbol] Attribute type (:string, :integer, :object, etc.) + # @param helpers [Array] Helper symbols (:required, :optional) + # @param nesting_level [Integer] Current nesting depth + # @param options [Hash] Attribute options (default:, format:, etc.) + # @param block [Proc] Block for nested attributes (object/array) + # @return [Treaty::Action::Response::Attribute::Attribute] Created attribute instance + def create_attribute(name, type, *helpers, nesting_level:, **options, &block) + Attribute.new( + name, + type, + *helpers, + nesting_level:, + **options, + &block + ) + end + + # Deep copies an attribute with adjusted nesting level + # + # Used when copying attributes from Entity classes via use_entity. + # Recursively copies nested attributes for object/array types. + # + # @param source_attribute [Treaty::Entity::Attribute::Base] Source attribute to copy + # @param new_nesting_level [Integer] Nesting level for copied attribute + # @return [Treaty::Action::Response::Attribute::Attribute] Deep copied attribute + def deep_copy_attribute(source_attribute, new_nesting_level) # rubocop:disable Metrics/MethodLength + copied = Attribute.new( + source_attribute.name, + source_attribute.type, + nesting_level: new_nesting_level, + **deep_copy_options(source_attribute.options) + ) + + return copied unless source_attribute.nested? + + source_attribute.collection_of_attributes.each do |nested_source| + nested_copied = deep_copy_attribute(nested_source, new_nesting_level + 1) + copied.collection_of_attributes << nested_copied + end + + copied + end + end + end + end + end +end diff --git a/lib/treaty/action/response/entity.rb b/lib/treaty/action/response/entity.rb new file mode 100644 index 0000000..4e5a887 --- /dev/null +++ b/lib/treaty/action/response/entity.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Response + # Internal entity class for response attribute definitions. + # + # ## Purpose + # + # Provides DSL interface for defining response attributes when using + # inline block syntax in treaty definitions. Serves as the anonymous + # class base when `response STATUS do ... end` is used. + # + # ## Usage + # + # Created internally by: + # - Response::Factory (when using inline DSL blocks) + # + # ## DSL Interface + # + # Includes Treaty::Entity::Attribute::DSL which provides: + # - Type methods: string, integer, boolean, date, time, datetime + # - Structure methods: object, array + # - Helper support: :required, :optional + # + # ## Difference from Treaty::Entity::Base + # + # While Treaty::Entity::Base creates standalone entity classes, + # Response::Entity creates Response-specific attributes with: + # - Optional by default behavior + # - Response::Attribute::Attribute instances + # + # ## Example + # + # # When you write: + # version 1 do + # response 201 do + # object :post do + # string :id + # string :title + # end + # end + # end + # + # # Factory creates: Class.new(Response::Entity) + # # and calls string, object etc. on it + class Entity + include Treaty::Entity::Attribute::DSL + + class << self + private + + # Creates response-specific attribute instances + # + # Called by DSL methods (string, integer, etc.) to create + # Response::Attribute::Attribute instead of generic attributes. + # + # @param name [Symbol] Attribute name + # @param type [Symbol] Attribute type + # @param helpers [Array] Helper symbols (:required, :optional) + # @param nesting_level [Integer] Current nesting depth + # @param options [Hash] Attribute options + # @param block [Proc] Block for nested attributes + # @return [Treaty::Action::Response::Attribute::Attribute] Created attribute + def create_attribute(name, type, *helpers, nesting_level:, **options, &block) + Attribute::Attribute.new( + name, + type, + *helpers, + nesting_level:, + **options, + &block + ) + end + end + end + end + end +end diff --git a/lib/treaty/action/response/factory.rb b/lib/treaty/action/response/factory.rb new file mode 100644 index 0000000..017661a --- /dev/null +++ b/lib/treaty/action/response/factory.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Response + # Factory for creating response attribute collections. + # + # ## Purpose + # + # Captures response attribute definitions from treaty DSL and provides + # access to the resulting attribute collection. Supports both inline + # block syntax and Entity class references. + # + # ## Difference from Request::Factory + # + # Response::Factory stores HTTP status code along with attributes. + # This allows treaty to validate responses against expected status. + # + # ## Usage + # + # Created internally by: + # - Versions::Factory (when `response STATUS do ... end` is called) + # + # Consumed by: + # - Response::Validator (to validate service output) + # - Info::Builder (to build response schema information) + # - Versions::Execution::Base (to set response status) + # + # ## Definition Modes + # + # ### Inline Block Mode + # + # response 201 do + # object :post do + # string :id + # string :title + # end + # end + # + # ### Entity Class Mode + # + # response 201, Posts::Create::ResponseEntity + # + # ## Example + # + # factory = Response::Factory.new(201) + # factory.status # => 201 + # factory.object :post do + # factory.string :id + # end + # factory.collection_of_attributes # => Collection with post attribute + class Factory + # @return [Integer] HTTP status code for this response + attr_reader :status + + # Creates new response factory with HTTP status + # + # @param status [Integer] HTTP status code (200, 201, 404, etc.) + def initialize(status) + @status = status + end + + # Registers an Entity class for response schema + # + # Use this to reference a pre-defined Entity class instead of + # inline attribute definitions. + # + # @param entity_class [Class] Must be Treaty::Entity::Base subclass + # @raise [Treaty::Exceptions::Validation] If entity_class is invalid + # @return [void] + def use_entity(entity_class) + validate_entity_class!(entity_class) + @entity_class = entity_class + end + + # Returns the collection of defined attributes + # + # @return [Treaty::Entity::Attribute::Collection] Attribute collection + def collection_of_attributes + return Treaty::Entity::Attribute::Collection.new if @entity_class.nil? + + @entity_class.collection_of_attributes + end + + # Forwards DSL method calls to internal Entity class + # + # Creates an anonymous Entity class on first call, then forwards + # all DSL methods (string, integer, object, etc.) to it. + # + # @param type [Symbol] Attribute type (method name) + # @param helpers [Array] Helper symbols and arguments + # @param options [Hash] Attribute options + # @param block [Proc] Block for nested attributes + # @return [void] + def method_missing(type, *helpers, **options, &block) + @entity_class ||= Class.new(Entity) + + @entity_class.public_send(type, *helpers, **options, &block) + end + + # Checks if method should be handled by method_missing + # + # @param name [Symbol] Method name + # @return [Boolean] + def respond_to_missing?(name, *) + super + end + + private + + # Validates that entity_class is a Treaty::Entity::Base subclass + # + # @param entity_class [Class] Class to validate + # @raise [Treaty::Exceptions::Validation] If validation fails + # @return [void] + def validate_entity_class!(entity_class) + return if entity_class.is_a?(Class) && entity_class < Treaty::Entity::Base + + raise Treaty::Exceptions::Validation, + I18n.t( + "treaty.response.factory.invalid_entity_class", + type: entity_class.class, + value: entity_class + ) + end + end + end + end +end diff --git a/lib/treaty/action/response/validator.rb b/lib/treaty/action/response/validator.rb new file mode 100644 index 0000000..602b1ed --- /dev/null +++ b/lib/treaty/action/response/validator.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Response + # Validates service response data against schema. + # + # ## Purpose + # + # Validates response data from service execution against the response + # schema defined in the treaty version. Ensures service output matches + # the documented API contract. + # + # ## Usage + # + # Called internally by: + # - Versions::Execution::Response (after service execution) + # + # ## Validation Flow + # + # 1. Check if response schema is defined + # 2. Create dynamic Orchestrator with version's response attributes + # 3. Run validation pipeline (validate + transform) + # 4. Return validated/transformed data or raise error + # + # ## Error Handling + # + # Raises Treaty::Exceptions::Validation with detailed messages + # including attribute path and specific validation failures. + # + # ## Difference from Request::Validator + # + # - Validates service output, not controller params + # - Response attributes are optional by default + # - Used after service execution, not before + # + # ## Example + # + # # Typically called via class method: + # validated = Response::Validator.validate!( + # version_factory: version_factory, + # response_data: service_result + # ) + # + # # validated contains transformed response for client + class Validator + class << self + # Validates response data + # + # @param version_factory [Treaty::Action::Versions::Factory] Version with response schema + # @param response_data [Hash] Response data from service + # @return [Hash] Validated and transformed response + # @raise [Treaty::Exceptions::Validation] If validation fails + def validate!(version_factory:, response_data: {}) + new(version_factory:, response_data:).validate! + end + end + + # Creates new validator instance + # + # @param version_factory [Treaty::Action::Versions::Factory] Version with response schema + # @param response_data [Hash] Response data to validate + def initialize(version_factory:, response_data: {}) + @version_factory = version_factory + @response_data = response_data + end + + # Runs validation pipeline + # + # @return [Hash] Validated and transformed response + # @raise [Treaty::Exceptions::Validation] If validation fails + def validate! + validate_response_attributes! + end + + private + + # Validates and transforms response data + # + # Creates dynamic Orchestrator class that uses version's response + # attributes for validation. Returns raw data if no schema defined. + # + # @return [Hash] Validated and transformed data + # @raise [Treaty::Exceptions::Validation] If validation fails + def validate_response_attributes! + return @response_data unless response_attributes_exist? + + orchestrator_class = Class.new(Treaty::Entity::Attribute::Validation::Orchestrator::Base) do + define_method(:collection_of_attributes) do + @version_factory.response_factory.collection_of_attributes + end + end + + orchestrator_class.validate!( + version_factory: @version_factory, + data: @response_data + ) + end + + # Checks if response schema is defined for this version + # + # @return [Boolean] True if response attributes exist + def response_attributes_exist? + return false if @version_factory.response_factory&.collection_of_attributes&.empty? + + @version_factory.response_factory.collection_of_attributes.exists? + end + end + end + end +end diff --git a/lib/treaty/action/result.rb b/lib/treaty/action/result.rb new file mode 100644 index 0000000..3929bb1 --- /dev/null +++ b/lib/treaty/action/result.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Treaty + module Action + # Value object returned from treaty execution. + # + # ## Purpose + # + # Encapsulates the result of a treaty call, containing validated + # response data, HTTP status code, and resolved version. This is + # the primary return type from `Treaty.call!`. + # + # ## Usage + # + # Created by: + # - Versions::Workspace (at the end of treaty execution) + # + # Consumed by: + # - Controller::DSL (to render JSON response) + # - Test assertions (to verify treaty behavior) + # + # ## Attributes + # + # | Attribute | Description | + # |-----------|-------------| + # | `data` | Validated and transformed response hash | + # | `status` | HTTP status code from response definition | + # | `version` | Resolved Gem::Version object | + # + # ## Example + # + # result = Posts::CreateTreaty.call!( + # version: "1", + # params: { post: { title: "Hello" } } + # ) + # + # result.data # => { post: { id: "abc", title: "Hello" } } + # result.status # => 201 + # result.version # => Gem::Version.new("1") + # + # # In controller: + # render json: result.data, status: result.status + class Result + # @return [Hash] Validated response data + attr_reader :data + + # @return [Integer] HTTP status code + attr_reader :status + + # @return [Gem::Version] Resolved API version + attr_reader :version + + # Creates a new result instance + # + # @param data [Hash] Validated response data + # @param status [Integer] HTTP status code + # @param version [Gem::Version] Resolved version + def initialize(data:, status:, version:) + @data = data + @status = status + @version = version + end + + # Returns human-readable representation for debugging + # + # @return [String] Inspection string with all attributes + def inspect + "#<#{self.class.name} #{draw_result}>" + end + + private + + # Formats attributes for inspect output + # + # @return [String] Formatted attribute string + def draw_result + "@data=#{@data.inspect}, @status=#{@status.inspect}, @version=#{@version.inspect}" + end + end + end +end diff --git a/lib/treaty/action/versions/collection.rb b/lib/treaty/action/versions/collection.rb new file mode 100644 index 0000000..f3ed3bc --- /dev/null +++ b/lib/treaty/action/versions/collection.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Versions + # Collection wrapper for sets of version factories. + # + # ## Purpose + # + # Provides a unified interface for working with collections of version factories. + # Uses Ruby Set internally for uniqueness but exposes Array-like interface. + # + # ## Usage + # + # Used internally by: + # - Versions::DSL (to store version factories) + # - Versions::Resolver (to find matching version) + # - Info::Builder (to build version information) + # + # ## Methods + # + # Delegates common collection methods to internal Set: + # - `<<` - Add version factory + # - `map` - Iteration with transformation + # - `find` - Access by condition + # + # ## Example + # + # collection = Collection.new + # collection << Factory.new(version: 1, default: true) + # collection << Factory.new(version: 2, default: false) + # collection.find { |f| f.default_result } # => Factory(v1) + class Collection + extend Forwardable + + def_delegators :@collection, :<<, :map, :find + + # Creates a new collection instance + # + # @param collection [Set] Initial collection (default: empty Set) + def initialize(collection = Set.new) + @collection = collection + end + end + end + end +end diff --git a/lib/treaty/action/versions/dsl.rb b/lib/treaty/action/versions/dsl.rb new file mode 100644 index 0000000..45aa00d --- /dev/null +++ b/lib/treaty/action/versions/dsl.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Versions + # DSL module for defining API versions in treaty classes. + # + # ## Purpose + # + # Provides the `version` class method for defining API versions. + # Each version can have its own request/response schema, executor, + # summary, and deprecation status. + # + # ## Usage + # + # Included in: + # - Treaty::Action::Base (as core DSL functionality) + # + # ## DSL Methods + # + # When included, provides: + # - `version` - Define a new API version with configuration block + # - `collection_of_versions` - Access all defined versions + # + # ## Version Definition + # + # class Posts::CreateTreaty < ApplicationTreaty + # version 1, default: true do + # summary "Initial version" + # + # request do + # object :post do + # string :title, :required + # end + # end + # + # response 201 do + # object :post do + # string :id + # end + # end + # + # delegate_to Posts::CreateService + # end + # + # version 2 do + # summary "Added tags" + # deprecated { ENV["V2_DEPRECATED"] == "true" } + # # ... + # end + # end + # + # ## Validation + # + # Validates that only one version is marked as default. + # Raises `VersionMultipleDefaults` if multiple defaults detected. + module DSL + # Hook called when module is included + # + # @param base [Class] The class including this module + def self.included(base) + base.extend(ClassMethods) + base.include(Workspace) + end + + # Class methods added to including class + module ClassMethods + private + + # Defines a new API version + # + # Creates a version factory, evaluates the configuration block, + # validates the configuration, and adds to collection. + # + # @param version [Integer, String, Array] Version identifier + # @param default [Boolean] Whether this is the default version + # @param block [Proc] Configuration block (request, response, delegate_to, etc.) + # @raise [Treaty::Exceptions::VersionMultipleDefaults] If multiple defaults + # @return [void] + def version(version, default: false, &block) + @version_factory = Factory.new(version:, default:) + + @version_factory.instance_eval(&block) + @version_factory.validate_after_block! + + validate_multiple_defaults! if @version_factory.default_result == true + + collection_of_versions << @version_factory + + @version_factory = nil + end + + # Returns collection of all defined versions + # + # @return [Treaty::Action::Versions::Collection] Collection of version factories + def collection_of_versions + @collection_of_versions ||= Collection.new + end + + # Validates that only one version is marked as default + # + # @raise [Treaty::Exceptions::VersionMultipleDefaults] If multiple defaults + # @return [void] + def validate_multiple_defaults! + existing_defaults = collection_of_versions.map(&:default_result).count(true) + + return if existing_defaults.zero? + + raise Treaty::Exceptions::VersionMultipleDefaults, + I18n.t("treaty.versioning.factory.multiple_defaults") + end + end + end + end + end +end diff --git a/lib/treaty/action/versions/execution/request.rb b/lib/treaty/action/versions/execution/request.rb new file mode 100644 index 0000000..32be8d5 --- /dev/null +++ b/lib/treaty/action/versions/execution/request.rb @@ -0,0 +1,287 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Versions + module Execution + # Executes the configured service/proc with validated parameters. + # + # ## Purpose + # + # Handles the actual execution of the delegated service, supporting + # multiple executor types: Class, String path, and Proc/Lambda. + # Provides special handling for Servactory services. + # + # ## Usage + # + # Called by: + # - Versions::Workspace (after request validation) + # + # ## Executor Types + # + # | Type | Example | Resolution | + # |------|---------|------------| + # | Class | `Posts::CreateService` | Direct call | + # | String | `"posts/create_service"` | Constantized then called | + # | Proc | `->(params:) { ... }` | Direct call | + # + # ## Servactory Integration + # + # Detects Servactory services via `servactory?` class method. + # Catches Servactory-specific exceptions and wraps them in + # `Treaty::Exceptions::Execution`. + # + # ## Execution Flow + # + # 1. Resolve executor (constantize string if needed) + # 2. Build call parameters (params + optional inventory) + # 3. Execute via appropriate method: + # - Proc: `executor.call(**params)` + # - Servactory: `executor.call!(**params)` + # - Regular: `executor.public_send(method, **params)` + # 4. Extract data from result (handles `.data` accessor) + # + # ## Example + # + # result = Execution::Request.execute!( + # version_factory: factory, + # validated_params: { post: { title: "Hello" } }, + # inventory: inventory_collection, + # context: controller + # ) + class Request # rubocop:disable Metrics/ClassLength + # Executes service with validated parameters (class method shortcut) + # + # @param version_factory [Treaty::Action::Versions::Factory] Version configuration + # @param validated_params [Hash] Validated request parameters + # @param inventory [Treaty::Action::Inventory::Collection, nil] Optional inventory + # @param context [Object, nil] Controller context for inventory + # @return [Hash] Service execution result + # @raise [Treaty::Exceptions::Execution] If execution fails + def self.execute!(...) + new(...).execute! + end + + # Creates a new execution request instance + # + # @param version_factory [Treaty::Action::Versions::Factory] Version with executor configuration + # @param validated_params [Hash] Validated request parameters + # @param inventory [Treaty::Action::Inventory::Collection, nil] Optional inventory + # @param context [Object, nil] Controller context for inventory evaluation + def initialize(version_factory:, validated_params:, inventory: nil, context: nil) + @inventory = inventory + @context = context + @version_factory = version_factory + @validated_params = validated_params + end + + # Executes the service and returns result + # + # @return [Hash] Execution result data + # @raise [Treaty::Exceptions::Execution] If executor missing or fails + def execute! + raise_executor_missing_error! if @version_factory.executor.nil? + + extract_data_from_result + end + + private + + # Extracts data from execution result + # + # Handles different result types: + # - Proc results returned directly + # - Objects with `.data` accessor unwrapped + # - Other results returned directly + # + # @return [Object] Extracted result data + def extract_data_from_result + return execution_result if executor.is_a?(Proc) + return execution_result.data if execution_result.respond_to?(:data) + + execution_result + end + + ######################################################################## + + # Executes and caches the service result + # + # @return [Object] Raw execution result + def execution_result + @execution_result ||= + if executor.is_a?(Proc) + execute_proc + elsif servactory_service? + execute_servactory + else + execute_regular_class + end + end + + ######################################################################## + + # Returns resolved executor (cached) + # + # @return [Class, Proc] Resolved executor + def executor + @executor ||= resolve_executor(@version_factory.executor.executor) + end + + ######################################################################## + + # Resolves executor from various input types + # + # @param executor [Class, String, Symbol, Proc] Executor reference + # @return [Class, Proc] Resolved executor + # @raise [Treaty::Exceptions::Execution] If resolution fails + def resolve_executor(executor) # rubocop:disable Metrics/MethodLength + return executor if executor.is_a?(Proc) || executor.is_a?(Class) + + if executor.is_a?(String) || executor.is_a?(Symbol) + string_executor = executor.to_s + + if string_executor.empty? + raise Treaty::Exceptions::Execution, + I18n.t("treaty.execution.executor_empty") + end + + constant_name = normalize_constant_name(executor) + + begin + constant_name.constantize + rescue NameError + raise Treaty::Exceptions::Execution, + I18n.t("treaty.execution.executor_not_found", class_name: constant_name) + end + else + raise Treaty::Exceptions::Execution, + I18n.t("treaty.execution.executor_invalid_type", type: executor.class) + end + end + + ######################################################################## + + # Normalizes string/symbol to constant name + # + # Handles path-style strings like "posts/create_service" + # converting to "Posts::CreateService". + # + # @param name [String, Symbol] Name to normalize + # @return [String] Constant name + def normalize_constant_name(name) + string = name.to_s + + return string if string.include?("::") + return string.split("/").map(&:camelize).join("::") if string.include?("/") + + string + end + + ######################################################################## + ######################################################################## + ######################################################################## + + # Creates inventory executor for lazy evaluation + # + # @return [Treaty::Action::Executor::Inventory] Inventory executor + def evaluated_inventory + @evaluated_inventory ||= Treaty::Action::Executor::Inventory.new(@inventory, @context) + end + + ######################################################################## + + # Executes Proc executor + # + # @return [Object] Proc result + # @raise [Treaty::Exceptions::Execution] If proc raises error + def execute_proc + executor.call(**build_call_params) + rescue StandardError => e + raise Treaty::Exceptions::Execution, + I18n.t("treaty.execution.proc_error", message: e.message) + end + + # Executes Servactory service + # + # Uses `call!` method and catches Servactory-specific exceptions. + # + # @return [Object] Service result + # @raise [Treaty::Exceptions::Execution] If service raises error + def execute_servactory # rubocop:disable Metrics/MethodLength + executor.call!(**build_call_params) + rescue Servactory::Exceptions::Input => e + raise Treaty::Exceptions::Execution, + I18n.t("treaty.execution.servactory_input_error", message: e.message) + rescue Servactory::Exceptions::Internal => e + raise Treaty::Exceptions::Execution, + I18n.t("treaty.execution.servactory_internal_error", message: e.message) + rescue Servactory::Exceptions::Output => e + raise Treaty::Exceptions::Execution, + I18n.t("treaty.execution.servactory_output_error", message: e.message) + rescue Servactory::Exceptions::Failure => e + raise Treaty::Exceptions::Execution, + I18n.t("treaty.execution.servactory_failure_error", message: e.message) + end + + # Executes regular class with configured method + # + # @return [Object] Method result + # @raise [Treaty::Exceptions::Execution] If method missing or raises error + def execute_regular_class # rubocop:disable Metrics/MethodLength + method_name = @version_factory.executor.method + + unless executor.respond_to?(method_name) + raise Treaty::Exceptions::Execution, + I18n.t( + "treaty.execution.method_not_found", + method: method_name, + class_name: executor + ) + end + + executor.public_send(method_name, **build_call_params) + rescue StandardError => e + raise Treaty::Exceptions::Execution, + I18n.t("treaty.execution.regular_service_error", message: e.message) + end + + ######################################################################## + ######################################################################## + ######################################################################## + + # Builds parameters hash for service call + # + # Includes validated params and optionally inventory if defined. + # + # @return [Hash] Call parameters + def build_call_params + if @inventory&.exists? + { params: @validated_params, inventory: evaluated_inventory } + else + { params: @validated_params } + end + end + + # Raises error when executor not configured + # + # @raise [Treaty::Exceptions::Execution] + def raise_executor_missing_error! + raise Treaty::Exceptions::Execution, + I18n.t( + "treaty.execution.executor_missing", + version: @version_factory.version + ) + end + + # Checks if executor is a Servactory service + # + # @return [Boolean] True if executor responds to servactory? + def servactory_service? + executor.respond_to?(:servactory?) && + executor.servactory? + end + end + end + end + end +end diff --git a/lib/treaty/action/versions/executor.rb b/lib/treaty/action/versions/executor.rb new file mode 100644 index 0000000..f3a3752 --- /dev/null +++ b/lib/treaty/action/versions/executor.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Versions + # Value object holding executor reference and method name. + # + # ## Purpose + # + # Stores the service class/proc and method name configured via + # `delegate_to` in version definitions. Separates executor + # configuration from execution logic. + # + # ## Usage + # + # Created by: + # - Versions::Factory (when `delegate_to` is called) + # + # Consumed by: + # - Versions::Execution::Request (to execute the service) + # + # ## Executor Types + # + # The `executor` attribute can hold: + # - Class reference: `Posts::CreateService` + # - String path: `"posts/create_service"` + # - Proc/Lambda: `->(params:) { ... }` + # + # ## Method Attribute + # + # The `method` attribute specifies which method to call: + # - Default: `:call` + # - Custom: specified via hash syntax `delegate_to Service => :perform` + # + # ## Example + # + # # In version definition: + # delegate_to Posts::CreateService # method defaults to :call + # delegate_to Posts::CreateService => :call! # explicit method + # + # # Creates: + # Executor.new(Posts::CreateService, :call) + class Executor + # @return [Class, String, Proc] Service class, path string, or proc + attr_reader :executor + + # @return [Symbol] Method name to call on executor + attr_reader :method + + # Creates a new executor value object + # + # @param executor [Class, String, Proc] Service reference + # @param method [Symbol] Method to invoke (default: :call) + def initialize(executor, method) + @executor = executor + @method = method + end + end + end + end +end diff --git a/lib/treaty/action/versions/factory.rb b/lib/treaty/action/versions/factory.rb new file mode 100644 index 0000000..7247216 --- /dev/null +++ b/lib/treaty/action/versions/factory.rb @@ -0,0 +1,253 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Versions + # Factory for version configuration and DSL. + # + # ## Purpose + # + # Provides the DSL interface within `version` blocks. Captures all + # version configuration: summary, deprecation, request schema, + # response schema, and executor delegation. + # + # ## Usage + # + # Created by: + # - Versions::DSL (when `version` is called) + # + # Consumed by: + # - Versions::Resolver (to find matching version) + # - Versions::Execution::Request (to execute the service) + # - Request::Validator / Response::Validator (to get schemas) + # - Info::Builder (to build version information) + # + # ## DSL Methods + # + # | Method | Purpose | + # |--------|---------| + # | `summary` | Set version description text | + # | `deprecated` | Mark version as deprecated (bool or proc) | + # | `request` | Define request schema (block or entity class) | + # | `response` | Define response schema with status code | + # | `delegate_to` | Set service class/proc to execute | + # + # ## Example + # + # version 1, default: true do + # summary "Initial API version" + # + # deprecated { ENV["V1_DEPRECATED"] == "true" } + # + # request do + # object :post do + # string :title, :required + # end + # end + # + # response 201 do + # object :post do + # string :id + # end + # end + # + # delegate_to Posts::CreateService + # end + # + # ## Validation + # + # - `default` must be boolean or Proc + # - Cannot be both default and deprecated + class Factory + # @return [Treaty::Action::Versions::Semantic] Semantic version wrapper + attr_reader :version + + # @return [Boolean] Whether this is the default version + attr_reader :default_result + + # @return [String, nil] Version summary/description + attr_reader :summary_text + + # @return [Boolean] Whether version is deprecated + attr_reader :deprecated_result + + # @return [Treaty::Action::Versions::Executor, nil] Executor configuration + attr_reader :executor + + # @return [Treaty::Action::Request::Factory, nil] Request schema factory + attr_reader :request_factory + + # @return [Treaty::Action::Response::Factory, nil] Response schema factory + attr_reader :response_factory + + # Creates a new version factory + # + # @param version [Integer, String, Array] Version identifier + # @param default [Boolean, Proc] Whether this is the default version + def initialize(version:, default:) + @version = Semantic.new(version) + @default_result = default.is_a?(Proc) ? default.call : default + @summary_text = nil + @deprecated_result = false + @executor = nil + + validate! + end + + # Validates configuration on creation + # + # @return [void] + def validate! + validate_default_option! + end + + # Validates configuration after block evaluation + # + # @raise [Treaty::Exceptions::VersionDefaultDeprecatedConflict] + # @return [void] + def validate_after_block! + validate_default_deprecated_conflict! + end + + # Sets version summary text + # + # @param text [String] Version description + # @return [void] + def summary(text) + @summary_text = text + end + + # Marks version as deprecated + # + # Accepts boolean, Proc, or block that evaluates to boolean. + # Deprecated versions raise error when accessed. + # + # @param condition [Boolean, Proc, nil] Deprecation condition + # @yield Block that returns boolean + # @return [void] + def deprecated(condition = nil) + result = + if condition.is_a?(Proc) + condition.call + elsif condition.is_a?(TrueClass) || condition.is_a?(FalseClass) + condition + else + yield + end + + @deprecated_result = result + end + + # Defines request schema + # + # Accepts either an Entity class or a block with attribute definitions. + # + # @param entity_class [Class, nil] Entity class for request schema + # @yield Block with request attribute definitions + # @return [void] + def request(entity_class = nil, &block) + @request_factory ||= Request::Factory.new + + if entity_class.present? + @request_factory.use_entity(entity_class) + elsif block_given? + @request_factory.instance_eval(&block) + end + end + + # Defines response schema with HTTP status + # + # Accepts status code and either Entity class or block. + # + # @param status [Integer] HTTP status code (200, 201, 404, etc.) + # @param entity_class [Class, nil] Entity class for response schema + # @yield Block with response attribute definitions + # @return [void] + def response(status, entity_class = nil, &block) + @response_factory ||= Response::Factory.new(status) + + if entity_class.present? + @response_factory.use_entity(entity_class) + elsif block_given? + @response_factory.instance_eval(&block) + end + end + + # Configures service delegation + # + # Sets the executor (class, string, or proc) and method to call. + # + # @param executor [Class, String, Proc, Hash] Service reference + # @param method [Symbol] Method to call (default: :call) + # @return [void] + # + # @example Class reference + # delegate_to Posts::CreateService + # + # @example With custom method + # delegate_to Posts::CreateService => :call! + # + # @example String path + # delegate_to "posts/create_service" + # + # @example Lambda + # delegate_to ->(params:) { { id: SecureRandom.uuid } } + def delegate_to(executor, method = :call) + @executor = Executor.new(executor, method) + end + + ########################################################################## + + private + + # Validates default option is boolean or Proc + # + # @raise [Treaty::Exceptions::Validation] If invalid type + # @return [Boolean, Proc] + def validate_default_option! + if @default_result.is_a?(TrueClass) || @default_result.is_a?(FalseClass) || @default_result.is_a?(Proc) + return @default_result + end + + raise Treaty::Exceptions::Validation, + I18n.t( + "treaty.versioning.factory.invalid_default_option", + type: @default_result.class + ) + end + + # Validates version is not both default and deprecated + # + # @raise [Treaty::Exceptions::VersionDefaultDeprecatedConflict] + # @return [void] + def validate_default_deprecated_conflict! + return unless @default_result == true + return unless @deprecated_result == true + + raise Treaty::Exceptions::VersionDefaultDeprecatedConflict, + I18n.t( + "treaty.versioning.factory.default_deprecated_conflict", + version: @version.version + ) + end + + ########################################################################## + + # Catches unknown DSL methods with helpful error + # + # @raise [Treaty::Exceptions::MethodName] + def method_missing(name, *, &_block) + raise Treaty::Exceptions::MethodName, + I18n.t("treaty.versioning.factory.unknown_method", method: name) + end + + # Required for method_missing + # + # @return [Boolean] + def respond_to_missing?(name, *) + super + end + end + end + end +end diff --git a/lib/treaty/action/versions/resolver.rb b/lib/treaty/action/versions/resolver.rb new file mode 100644 index 0000000..6686350 --- /dev/null +++ b/lib/treaty/action/versions/resolver.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Versions + # Resolves version factory based on specified or default version. + # + # ## Purpose + # + # Finds the appropriate version factory from the collection based on: + # - Specified version (from request header or parameter) + # - Default version (if no version specified) + # + # Also enforces deprecation by raising error for deprecated versions. + # + # ## Usage + # + # Called internally by: + # - Versions::Workspace (at the start of treaty execution) + # + # ## Resolution Logic + # + # 1. If version specified → find matching factory or raise VersionNotFound + # 2. If no version → use default factory or raise SpecifiedVersionNotFound + # 3. If resolved factory is deprecated → raise Deprecated + # + # ## Error Types + # + # | Error | Condition | + # |-------|-----------| + # | VersionNotFound | Specified version doesn't exist | + # | SpecifiedVersionNotFound | No version specified and no default | + # | Deprecated | Resolved version is marked deprecated | + # + # ## Example + # + # # Find specific version: + # factory = Resolver.resolve!( + # specified_version: "2", + # collection_of_versions: collection + # ) + # + # # Use default version: + # factory = Resolver.resolve!( + # specified_version: nil, + # collection_of_versions: collection + # ) + class Resolver + # Resolves version factory (class method shortcut) + # + # @param specified_version [String, nil] Requested version or nil for default + # @param collection_of_versions [Treaty::Action::Versions::Collection] Available versions + # @return [Treaty::Action::Versions::Factory] Resolved version factory + # @raise [Treaty::Exceptions::VersionNotFound] If version doesn't exist + # @raise [Treaty::Exceptions::SpecifiedVersionNotFound] If no default available + # @raise [Treaty::Exceptions::Deprecated] If version is deprecated + def self.resolve!(...) + new(...).resolve! + end + + # Creates a new resolver instance + # + # @param specified_version [String, nil] Requested version + # @param collection_of_versions [Treaty::Action::Versions::Collection] Available versions + def initialize(specified_version:, collection_of_versions:) + @specified_version = specified_version + @collection_of_versions = collection_of_versions + end + + # Resolves and returns the appropriate version factory + # + # @return [Treaty::Action::Versions::Factory] Resolved version factory + # @raise [Treaty::Exceptions::VersionNotFound] If version doesn't exist + # @raise [Treaty::Exceptions::SpecifiedVersionNotFound] If no default available + # @raise [Treaty::Exceptions::Deprecated] If version is deprecated + def resolve! + determined_factory = + if specified_version_blank? + default_version_factory || raise_specified_version_not_found! + else + version_factory || raise_version_not_found! + end + + raise_version_deprecated! if determined_factory.deprecated_result + + determined_factory + end + + private + + # Finds factory matching specified version + # + # @return [Factory, nil] Matching factory or nil + def version_factory + @version_factory ||= + @collection_of_versions.find do |factory| + factory.version.version == @specified_version + end + end + + # Finds default version factory + # + # @return [Factory, nil] Default factory or nil + def default_version_factory + @default_version_factory ||= + @collection_of_versions.find(&:default_result) + end + + # Checks if specified version is blank + # + # @return [Boolean] True if version is nil or empty string + def specified_version_blank? + @specified_version.to_s.strip.empty? + end + + ########################################################################## + + # Raises error when no version specified and no default exists + # + # @raise [Treaty::Exceptions::SpecifiedVersionNotFound] + def raise_specified_version_not_found! + raise Treaty::Exceptions::SpecifiedVersionNotFound, + I18n.t("treaty.versioning.resolver.specified_version_required") + end + + # Raises error when specified version doesn't exist + # + # @raise [Treaty::Exceptions::VersionNotFound] + def raise_version_not_found! + raise Treaty::Exceptions::VersionNotFound, + I18n.t( + "treaty.versioning.resolver.version_not_found", + version: @specified_version + ) + end + + # Raises error when resolved version is deprecated + # + # @raise [Treaty::Exceptions::Deprecated] + def raise_version_deprecated! + raise Treaty::Exceptions::Deprecated, + I18n.t( + "treaty.versioning.resolver.version_deprecated", + version: @specified_version + ) + end + end + end + end +end diff --git a/lib/treaty/action/versions/semantic.rb b/lib/treaty/action/versions/semantic.rb new file mode 100644 index 0000000..75eeb47 --- /dev/null +++ b/lib/treaty/action/versions/semantic.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Versions + # Semantic version wrapper using Gem::Version. + # + # ## Purpose + # + # Normalizes version inputs (Integer, String, Array) into a consistent + # Gem::Version object. This enables proper version comparison and + # semantic versioning support. + # + # ## Usage + # + # Created internally by: + # - Versions::Factory (to store version number) + # + # Consumed by: + # - Versions::Resolver (to match requested version) + # - Info::Builder (to display version info) + # + # ## Version Formats + # + # Accepts multiple input formats: + # - Integer: `1` → "1" + # - String: `"1.0.0"` → "1.0.0" + # - Array: `[1, 0, 0]` → "1.0.0" + # + # ## Underlying Implementation + # + # Uses Gem::Version internally which provides: + # - Semantic version comparison (1.0.0 < 1.0.1 < 1.1.0 < 2.0.0) + # - Proper handling of pre-release versions (1.0.0-alpha < 1.0.0) + # - String normalization + # + # ## Example + # + # Semantic.new(1).version # => Gem::Version.new("1") + # Semantic.new("1.2.3").version # => Gem::Version.new("1.2.3") + # Semantic.new([1, 2, 3]).version # => Gem::Version.new("1.2.3") + class Semantic + # @return [Gem::Version] Normalized semantic version + attr_reader :version + + # Creates a new semantic version wrapper + # + # @param version [Integer, String, Array] Version in any supported format + def initialize(version) + version = + if version.is_a?(Array) + version.join(".") + # elsif version.is_a?(Integer) + # version.to_s + else + version # rubocop:disable Style/RedundantSelfAssignmentBranch + end + + @version = Gem::Version.new(version) + end + end + end + end +end diff --git a/lib/treaty/action/versions/workspace.rb b/lib/treaty/action/versions/workspace.rb new file mode 100644 index 0000000..c4a737c --- /dev/null +++ b/lib/treaty/action/versions/workspace.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +module Treaty + module Action + module Versions + # Core execution module for treaty call! method. + # + # ## Purpose + # + # Provides the `call!` instance method that orchestrates the complete + # treaty execution flow: version resolution, request validation, + # service execution, and response validation. + # + # ## Usage + # + # Included via: + # - Versions::DSL (when DSL is included in treaty class) + # + # Called by: + # - Controller integration (via Treaty::Action::Base.call!) + # + # ## Execution Flow + # + # ``` + # call!(context:, inventory:, version:, params:) + # │ + # ├─1─► Resolver.resolve! ──► Find version factory + # │ + # ├─2─► Request::Validator.validate! ──► Validate params + # │ + # ├─3─► Execution::Request.execute! ──► Run service + # │ + # ├─4─► Response::Validator.validate! ──► Validate response + # │ + # └─5─► Result.new ──► Return result object + # ``` + # + # ## Parameters + # + # | Parameter | Description | + # |-----------|-------------| + # | context | Controller instance | + # | inventory | Inventory::Collection with controller data | + # | version | Requested version string (or nil for default) | + # | params | Request parameters | + # + # ## Return Value + # + # Returns `Treaty::Action::Result` with: + # - `data` - Validated response hash + # - `status` - HTTP status code + # - `version` - Resolved version string + module Workspace + private + + # Executes the complete treaty flow + # + # Orchestrates version resolution, validation, execution, + # and returns a Result object. + # + # @param context [Object] Controller instance + # @param inventory [Treaty::Action::Inventory::Collection] Controller data + # @param version [String, nil] Requested version or nil for default + # @param params [Hash] Request parameters + # @return [Treaty::Action::Result] Execution result + # @raise [Treaty::Exceptions::VersionNotFound] If version not found + # @raise [Treaty::Exceptions::Deprecated] If version is deprecated + # @raise [Treaty::Exceptions::Validation] If validation fails + # @raise [Treaty::Exceptions::Execution] If service execution fails + def call!(context:, inventory:, version:, params:, **) # rubocop:disable Metrics/MethodLength + super + + version_factory = Resolver.resolve!( + specified_version: version, + collection_of_versions: @collection_of_versions + ) + + validated_params = Request::Validator.validate!( + params:, + version_factory: + ) + + executor_result = Execution::Request.execute!( + context:, + inventory:, + version_factory:, + validated_params: + ) + + validated_response = Response::Validator.validate!( + version_factory:, + response_data: executor_result + ) + + status = version_factory.response_factory&.status || 200 + + Result.new( + data: validated_response, + status:, + version: version_factory.version.version + ) + end + end + end + end +end diff --git a/lib/treaty/base.rb b/lib/treaty/base.rb deleted file mode 100644 index d3c27f7..0000000 --- a/lib/treaty/base.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module Treaty - class Base - include Info::Rest::DSL - include Context::DSL - include Versions::DSL - end -end diff --git a/lib/treaty/context/callable.rb b/lib/treaty/context/callable.rb deleted file mode 100644 index d90ab94..0000000 --- a/lib/treaty/context/callable.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Context - module Callable - def call!(version:, params:, context: nil, inventory: nil) - treaty_instance = send(:new) - - _call!(treaty_instance, context:, inventory:, version:, params:) - end - - private - - def _call!(treaty_instance, context:, inventory:, version:, params:) - treaty_instance.send( - :_call!, - context:, - inventory:, - version:, - params:, - collection_of_versions: - ) - end - end - end -end diff --git a/lib/treaty/context/dsl.rb b/lib/treaty/context/dsl.rb deleted file mode 100644 index f3a4c9c..0000000 --- a/lib/treaty/context/dsl.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Context - module DSL - def self.included(base) - base.extend(Callable) - base.include(Workspace) - end - end - end -end diff --git a/lib/treaty/context/workspace.rb b/lib/treaty/context/workspace.rb deleted file mode 100644 index 81646b1..0000000 --- a/lib/treaty/context/workspace.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Context - module Workspace - private - - def _call!( - context:, - inventory:, - version:, - params:, - collection_of_versions: - ) - call!( - context:, - inventory:, - version:, - params:, - collection_of_versions: - ) - end - - def call!( - collection_of_versions:, - ** - ) - @collection_of_versions = collection_of_versions - end - end - end -end diff --git a/lib/treaty/controller/dsl.rb b/lib/treaty/controller/dsl.rb index 5b7d091..809abc8 100644 --- a/lib/treaty/controller/dsl.rb +++ b/lib/treaty/controller/dsl.rb @@ -57,7 +57,7 @@ def treaty_version def treaty_build_inventory_for(action_name, block) return nil unless block - factory = Treaty::Inventory::Factory.new(action_name) + factory = Treaty::Action::Inventory::Factory.new(action_name) factory.instance_eval(&block) factory.collection end diff --git a/lib/treaty/entity/attribute/base.rb b/lib/treaty/entity/attribute/base.rb index 18d83c5..3618f6f 100644 --- a/lib/treaty/entity/attribute/base.rb +++ b/lib/treaty/entity/attribute/base.rb @@ -80,7 +80,7 @@ def initialize(name, type, *helpers, nesting_level: 0, **options, &block) # Returns collection of nested attributes for this attribute # - # @return [Collection] Collection of nested attributes + # @return [Treaty::Entity::Attribute::Collection] Collection of nested attributes def collection_of_attributes @collection_of_attributes ||= Collection.new end diff --git a/lib/treaty/entity/attribute/builder/base.rb b/lib/treaty/entity/attribute/builder/base.rb index 60282c5..c569c44 100644 --- a/lib/treaty/entity/attribute/builder/base.rb +++ b/lib/treaty/entity/attribute/builder/base.rb @@ -91,7 +91,7 @@ class Base # Creates a new builder instance # - # @param collection_of_attributes [Collection] Collection to add attributes to + # @param collection_of_attributes [Treaty::Entity::Attribute::Collection] Collection to add attributes to # @param nesting_level [Integer] Current nesting depth def initialize(collection_of_attributes, nesting_level) @collection_of_attributes = collection_of_attributes diff --git a/lib/treaty/entity/attribute/dsl.rb b/lib/treaty/entity/attribute/dsl.rb index 574fd4d..261d592 100644 --- a/lib/treaty/entity/attribute/dsl.rb +++ b/lib/treaty/entity/attribute/dsl.rb @@ -45,7 +45,7 @@ def attribute(name, type, *helpers, **options, &block) # Returns collection of attributes for this class # - # @return [Collection] Collection of attributes + # @return [Treaty::Entity::Attribute::Collection] Collection of attributes def collection_of_attributes @collection_of_attributes ||= Treaty::Entity::Attribute::Collection.new end diff --git a/lib/treaty/entity/base.rb b/lib/treaty/entity/base.rb index 9dffe2d..0b9da30 100644 --- a/lib/treaty/entity/base.rb +++ b/lib/treaty/entity/base.rb @@ -73,7 +73,7 @@ class << self # Creates an Entity::Attribute for this Entity class # - # @return [Entity::Attribute::Attribute] Created attribute instance + # @return [Treaty::Entity::Attribute::Attribute] Created attribute instance def create_attribute(name, type, *helpers, nesting_level:, **options, &block) Attribute::Attribute.new( name, diff --git a/lib/treaty/entity/builder.rb b/lib/treaty/entity/builder.rb index d9ae424..6ef30bc 100644 --- a/lib/treaty/entity/builder.rb +++ b/lib/treaty/entity/builder.rb @@ -2,10 +2,64 @@ module Treaty module Entity - # Entity-specific attribute builder + # DSL builder for defining entity attributes. + # + # ## Purpose + # + # Provides Entity-specific implementation of the attribute builder. + # Creates Entity::Attribute::Attribute instances with **required by default** + # behavior (unlike Request/Response which have different defaults). + # + # ## Inheritance + # + # Extends Treaty::Entity::Attribute::Builder::Base which provides: + # - DSL interface (string, integer, object, array, etc.) + # - method_missing magic for type-based method calls + # - Helper support (:required, :optional) + # - Entity reuse via use_entity + # + # ## Usage + # + # Used internally by: + # - Entity::Attribute::Attribute (when processing nested object/array blocks) + # + # ## Default Behavior + # + # Entity attributes default to **required: true**, meaning all fields + # must be present unless explicitly marked `:optional`. + # + # ## DSL Example + # + # class UserEntity < Treaty::Entity::Base + # object :user do + # string :id # required by default + # string :name # required by default + # string :bio, :optional # explicitly optional + # object :profile do + # string :avatar_url + # end + # end + # end + # + # ## Methods + # + # Implements abstract methods from base class: + # - `create_attribute` - Creates Entity::Attribute::Attribute + # - `deep_copy_attribute` - Deep copies attribute for use_entity support class Builder < Treaty::Entity::Attribute::Builder::Base private + # Creates a new entity attribute instance + # + # Called by base class when defining attributes via DSL. + # + # @param name [Symbol] Attribute name + # @param type [Symbol] Attribute type (:string, :integer, :object, etc.) + # @param helpers [Array] Helper symbols (:required, :optional) + # @param nesting_level [Integer] Current nesting depth + # @param options [Hash] Attribute options (default:, format:, etc.) + # @param block [Proc] Block for nested attributes (object/array) + # @return [Treaty::Entity::Attribute::Attribute] Created attribute instance def create_attribute(name, type, *helpers, nesting_level:, **options, &block) Treaty::Entity::Attribute::Attribute.new( name, @@ -17,11 +71,14 @@ def create_attribute(name, type, *helpers, nesting_level:, **options, &block) ) end - # Deep copies an attribute with adjusted nesting level for Entity context. + # Deep copies an attribute with adjusted nesting level + # + # Used when copying attributes from other Entity classes via use_entity. + # Recursively copies nested attributes for object/array types. # - # @param source_attribute [Treaty::Entity::Attribute::Base] Attribute to copy - # @param new_nesting_level [Integer] New nesting level - # @return [Treaty::Entity::Attribute::Attribute] Copied attribute + # @param source_attribute [Treaty::Entity::Attribute::Base] Source attribute to copy + # @param new_nesting_level [Integer] Nesting level for copied attribute + # @return [Treaty::Entity::Attribute::Attribute] Deep copied attribute def deep_copy_attribute(source_attribute, new_nesting_level) # rubocop:disable Metrics/MethodLength copied = Treaty::Entity::Attribute::Attribute.new( source_attribute.name, diff --git a/lib/treaty/executor/inventory.rb b/lib/treaty/executor/inventory.rb deleted file mode 100644 index c93b603..0000000 --- a/lib/treaty/executor/inventory.rb +++ /dev/null @@ -1,122 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Executor - # Inventory wrapper that provides method-based access to inventory items. - # - # ## Purpose - # - # Wraps inventory collection and controller context, providing lazy evaluation - # of inventory items through method calls. This encapsulates all inventory logic - # within the class and provides a clean API for services. - # - # ## Usage - # - # ```ruby - # # Created internally by Treaty - # inventory = Treaty::Executor::Inventory.new(inventory_collection, controller_context) - # - # # Access via method calls - evaluates lazily - # inventory.posts # => Calls controller method or evaluates proc - # inventory.current_user # => Returns evaluated value - # - # # Raises exception for missing items - # inventory.missing_item # => Treaty::Exceptions::Inventory - # - # # Convert to hash - evaluates all items - # inventory.to_h # => { posts: [...], current_user: ... } - # ``` - # - # ## Architecture - # - # The class encapsulates: - # - Inventory collection (from controller's treaty block) - # - Controller context (for method calls and proc evaluation) - # - Lazy evaluation logic (items evaluated on access) - # - # ## Error Handling - # - # If an inventory item is not found, raises `Treaty::Exceptions::Inventory` with - # an I18n-translated error message listing available items. - class Inventory - # Creates a new inventory instance - # - # @param inventory [Treaty::Inventory::Collection] Collection of inventory items - # @param context [Object] Controller instance for evaluation - def initialize(inventory, context) - @inventory = inventory - @context = context - @evaluated_cache = {} - end - - # Provides method-based access to inventory items with lazy evaluation - # - # @param method_name [Symbol] The inventory item name - # @param _args [Array] Arguments (not used, for compatibility) - # @return [Object] The evaluated inventory item value - # @raise [Treaty::Exceptions::Inventory] If item not found - def method_missing(method_name, *_args) - # Check cache first - return @evaluated_cache[method_name] if @evaluated_cache.key?(method_name) - - # Find inventory item - item = find_inventory_item(method_name) - - # Evaluate and cache - @evaluated_cache[method_name] = item.evaluate(@context) - end - - # Checks if inventory responds to a method - # - # @param method_name [Symbol] The method name to check - # @param include_private [Boolean] Whether to include private methods - # @return [Boolean] True if inventory has the item - def respond_to_missing?(method_name, include_private = false) - return false if @inventory.nil? - - @inventory.names.include?(method_name) || super - end - - # Converts inventory to hash, evaluating all items - # - # @return [Hash] Hash of all evaluated inventory items - def to_h - return {} if @inventory.nil? - - @inventory.evaluate(@context) - end - - # Returns string representation - # - # @return [String] Inventory description - def inspect - items = @inventory&.names || [] - "#" - end - - private - - # Finds inventory item by name - # - # @param name [Symbol] Inventory item name - # @return [Treaty::Inventory::Inventory] The inventory item - # @raise [Treaty::Exceptions::Inventory] If not found or inventory is nil - def find_inventory_item(name) - # Use find method for cleaner search - item = @inventory&.find { |item| item.name == name } - - return item if item - - # Item not found - list available items - available = @inventory&.names || [] - - raise Treaty::Exceptions::Inventory, - I18n.t( - "treaty.executor.inventory.item_not_found", - name:, - available: available.join(", ") - ) - end - end - end -end diff --git a/lib/treaty/inventory/collection.rb b/lib/treaty/inventory/collection.rb deleted file mode 100644 index a60b6aa..0000000 --- a/lib/treaty/inventory/collection.rb +++ /dev/null @@ -1,71 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Inventory - # Collection wrapper for sets of inventory items. - # - # ## Purpose - # - # Provides a unified interface for working with collections of inventory items. - # Uses Ruby Set internally for uniqueness but exposes Array-like interface. - # - # ## Usage - # - # Used internally by: - # - Inventory::Factory (to store inventory items during DSL processing) - # - # ## Methods - # - # Delegates common collection methods to internal Set: - # - `<<` - Add inventory item - # - `empty?` - Check if collection is empty - # - # Custom methods: - # - `exists?` - Returns true if collection is not empty - # - `evaluate` - Evaluates all inventory items with context - # - # ## Example - # - # collection = Collection.new - # collection << Inventory.new(name: :posts, source: :load_posts) - # collection << Inventory.new(name: :meta, source: -> { { count: 10 } }) - # collection.size # => 2 - # collection.exists? # => true - class Collection - extend Forwardable - - def_delegators :@collection, :<<, :each_with_object, :find, :empty? - - # Creates a new collection instance - # - # @param collection [Set] Initial collection (default: empty Set) - def initialize(collection = Set.new) - @collection = collection - end - - # Checks if collection has any elements - # - # @return [Boolean] True if collection is not empty - def exists? - !empty? - end - - # Returns array of all inventory item names - # - # @return [Array] Array of inventory item names - def names - @collection.each_with_object([]) { |item, names| names << item.name } - end - - # Evaluates all inventory items with the given context - # - # @param context [Object] The controller instance to call methods on - # @return [Hash{Symbol => Object}] Hash of inventory name => resolved value - def evaluate(context) - @collection.each_with_object({}) do |inventory_item, hash| - hash[inventory_item.name] = inventory_item.evaluate(context) - end - end - end - end -end diff --git a/lib/treaty/inventory/factory.rb b/lib/treaty/inventory/factory.rb deleted file mode 100644 index a37e1ea..0000000 --- a/lib/treaty/inventory/factory.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Inventory - # Factory for building inventory collections via DSL. - # - # ## Purpose - # - # Provides the `provide` DSL method for defining inventory items in controller blocks. - # Captures calls like `provide :posts, from: :load_posts` and builds a collection. - # - # ## Usage - # - # Used internally by Controller::DSL when processing treaty blocks: - # - # ```ruby - # treaty :index do - # provide :posts, from: :load_posts # Explicit source - # provide :meta, from: -> { { count: 10 } } # Lambda source - # provide :current_user # Shorthand: uses :current_user as source - # end - # ``` - # - # ## Valid Sources - # - # - Symbol: Method name to call on controller (e.g., `:load_posts`) - # - Proc/Lambda: Callable object (e.g., `-> { Post.all }`) - # - Direct value: String, number, or any other value (e.g., `"Welcome"`) - # - Omitted: Uses inventory name as source (e.g., `provide :posts` → `from: :posts`) - # - # ## Invalid Sources - # - # - Direct method calls without symbol/proc (e.g., `from: load_posts`) - # - Explicit nil values (e.g., `from: nil`) - class Factory - attr_reader :collection - - def initialize(action_name) - @action_name = action_name - @collection = Collection.new - end - - # Handles the `provide` DSL method via method_missing - # - # @param method_name [Symbol] Should be :provide - # @param args [Array] First argument is the inventory name - # @param options [Hash] Optional :from key with source (defaults to inventory name) - # @return [Collection] The collection being built - # @raise [Treaty::Exceptions::Inventory] For invalid method or missing parameters - def method_missing(method_name, *args, **options, &_block) # rubocop:disable Metrics/MethodLength - # Only handle 'provide' method - unless method_name == :provide - raise Treaty::Exceptions::Inventory, - I18n.t( - "treaty.inventory.unknown_method", - method: method_name, - action: @action_name - ) - end - - # Extract inventory name - inventory_name = args.first - - unless inventory_name.is_a?(Symbol) - raise Treaty::Exceptions::Inventory, - I18n.t( - "treaty.inventory.name_must_be_symbol", - name: inventory_name.inspect - ) - end - - # Extract source from options (default to inventory name if not provided) - source = if options.key?(:from) - options.fetch(:from) - else - inventory_name - end - - # Create and add inventory item to collection - @collection << Inventory.new(name: inventory_name, source:) - - # Return collection for potential chaining - @collection - end - - def respond_to_missing?(method_name, *) - method_name == :provide || super - end - end - end -end diff --git a/lib/treaty/inventory/inventory.rb b/lib/treaty/inventory/inventory.rb deleted file mode 100644 index 9371e16..0000000 --- a/lib/treaty/inventory/inventory.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Inventory - # Represents a single inventory item that provides data to the treaty execution. - # - # An inventory item has a name and a source. The source can be: - # - Symbol: A method name to call on the controller (e.g., :load_posts) - # - Proc/Lambda: A callable object (e.g., -> { Post.all }) - # - Direct value: Any other value to pass directly (e.g., "text", 42) - # - # ## Usage - # - # ```ruby - # # In controller - # treaty :index do - # provide :posts, from: :load_posts - # provide :meta, from: -> { { count: 10 } } - # provide :title, from: "Welcome" - # end - # ``` - class Inventory - attr_reader :name, :source - - def initialize(name:, source:) - validate_name!(name) - validate_source!(source) - - @name = name - @source = source - end - - # Evaluates the inventory source with the given context - # - # @param context [Object] The controller instance to call methods on - # @return [Object] The resolved value - # @raise [Treaty::Exceptions::Inventory] If evaluation fails - def evaluate(context) # rubocop:disable Metrics/MethodLength - case source - when Symbol - evaluate_symbol(context) - when Proc - evaluate_proc(context) - else - source - end - rescue StandardError => e - raise Treaty::Exceptions::Inventory, - I18n.t( - "treaty.inventory.evaluation_error", - name: @name, - error: e.message - ) - end - - private - - # Evaluates Symbol source by calling method on context - # - # @param context [Object] The controller instance - # @return [Object] The method return value - def evaluate_symbol(context) - context.send(source) - end - - # Evaluates Proc source within controller context - # - # @param context [Object] The controller instance - # @return [Object] The proc return value - def evaluate_proc(context) - # Execute proc in controller context to access instance variables and methods - context.instance_exec(&source) - end - - def validate_name!(name) - return if name.is_a?(Symbol) && !name.to_s.empty? - - raise Treaty::Exceptions::Inventory, - I18n.t("treaty.inventory.invalid_name", name: name.inspect) - end - - def validate_source!(source) - # Source must be Symbol, Proc, or any other direct value - # We don't allow nil as it's likely a mistake - return unless source.nil? - - raise Treaty::Exceptions::Inventory, - I18n.t("treaty.inventory.source_required") - end - end - end -end diff --git a/lib/treaty/request/attribute/attribute.rb b/lib/treaty/request/attribute/attribute.rb deleted file mode 100644 index 4c60d99..0000000 --- a/lib/treaty/request/attribute/attribute.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Request - module Attribute - # Request-specific attribute that defaults to required: true - class Attribute < Treaty::Entity::Attribute::Base - private - - def apply_defaults! - # For request: required by default (true). - # message: nil means use I18n default message from validators - @options[:required] ||= { is: true, message: nil } - end - - def process_nested_attributes(&block) - return unless object_or_array? - - builder = Builder.new(collection_of_attributes, @nesting_level + 1) - builder.instance_eval(&block) - end - end - end - end -end diff --git a/lib/treaty/request/attribute/builder.rb b/lib/treaty/request/attribute/builder.rb deleted file mode 100644 index e026d96..0000000 --- a/lib/treaty/request/attribute/builder.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Request - module Attribute - # Request-specific attribute builder - class Builder < Treaty::Entity::Attribute::Builder::Base - private - - def create_attribute(name, type, *helpers, nesting_level:, **options, &block) - Attribute.new( - name, - type, - *helpers, - nesting_level:, - **options, - &block - ) - end - - # Deep copies an attribute with adjusted nesting level for Request context. - # - # @param source_attribute [Treaty::Entity::Attribute::Base] Attribute to copy - # @param new_nesting_level [Integer] New nesting level - # @return [Request::Attribute::Attribute] Copied attribute - def deep_copy_attribute(source_attribute, new_nesting_level) # rubocop:disable Metrics/MethodLength - copied = Attribute.new( - source_attribute.name, - source_attribute.type, - nesting_level: new_nesting_level, - **deep_copy_options(source_attribute.options) - ) - - return copied unless source_attribute.nested? - - source_attribute.collection_of_attributes.each do |nested_source| - nested_copied = deep_copy_attribute(nested_source, new_nesting_level + 1) - copied.collection_of_attributes << nested_copied - end - - copied - end - end - end - end -end diff --git a/lib/treaty/request/entity.rb b/lib/treaty/request/entity.rb deleted file mode 100644 index d6c930c..0000000 --- a/lib/treaty/request/entity.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Request - # Entity class for request definitions. - # Attributes are required by default. - # - # This class is used internally when defining request blocks. - # When you write a request block, Treaty creates an anonymous - # class based on Request::Entity. - class Entity - include Treaty::Entity::Attribute::DSL - - class << self - private - - # Creates a Request::Attribute::Attribute for this Request::Entity class - # - # @return [Request::Attribute::Attribute] Created attribute instance - def create_attribute(name, type, *helpers, nesting_level:, **options, &block) - Attribute::Attribute.new( - name, - type, - *helpers, - nesting_level:, - **options, - &block - ) - end - end - end - end -end diff --git a/lib/treaty/request/factory.rb b/lib/treaty/request/factory.rb deleted file mode 100644 index 4532b74..0000000 --- a/lib/treaty/request/factory.rb +++ /dev/null @@ -1,81 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Request - # Factory for creating request definitions. - # - # Supports two modes: - # 1. Block mode: Creates an anonymous Request::Entity class with the block - # 2. Entity mode: Uses a provided Entity class directly - # - # ## Block Mode - # - # ```ruby - # request do - # object :post do - # string :title - # end - # end - # ``` - # - # ## Entity Mode - # - # ```ruby - # request Posts::Create::RequestEntity - # ``` - class Factory - # Uses a provided Entity class - # - # @param entity_class [Class] Entity class to use - # @return [void] - # @raise [Treaty::Exceptions::Validation] if entity_class is not a valid Treaty::Entity::Base subclass - def use_entity(entity_class) - validate_entity_class!(entity_class) - @entity_class = entity_class - end - - # Returns collection of attributes from the entity class - # - # @return [Collection] Collection of attributes - def collection_of_attributes - return Treaty::Entity::Attribute::Collection.new if @entity_class.nil? - - @entity_class.collection_of_attributes - end - - # Handles DSL methods for defining attributes - # - # This allows the factory to be used with method_missing - # for backwards compatibility with direct method calls. - # Creates an anonymous Request::Entity class on first use. - def method_missing(type, *helpers, **options, &block) - # If no entity class yet, create one - @entity_class ||= Class.new(Entity) - - # Call the method on the entity class - @entity_class.public_send(type, *helpers, **options, &block) - end - - def respond_to_missing?(name, *) - super - end - - private - - # Validates that the provided entity_class is a valid Treaty::Entity::Base subclass - # - # @param entity_class [Class] Entity class to validate - # @raise [Treaty::Exceptions::Validation] if entity_class is not a valid Treaty::Entity::Base subclass - def validate_entity_class!(entity_class) - return if entity_class.is_a?(Class) && entity_class < Treaty::Entity::Base - - raise Treaty::Exceptions::Validation, - I18n.t( - "treaty.request.factory.invalid_entity_class", - type: entity_class.class, - value: entity_class - ) - end - end - end -end diff --git a/lib/treaty/request/validator.rb b/lib/treaty/request/validator.rb deleted file mode 100644 index 76cd5b6..0000000 --- a/lib/treaty/request/validator.rb +++ /dev/null @@ -1,60 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Request - # Validator for request data - class Validator - class << self - # Validates request parameters against the request definition - # - # @param params [Hash] Request parameters to validate - # @param version_factory [Versions::Factory] Version factory with request definition - # @return [Hash] Validated and transformed parameters - def validate!(params:, version_factory:) - new(params:, version_factory:).validate! - end - end - - def initialize(params:, version_factory:) - @params = params - @version_factory = version_factory - end - - def validate! - validate_request_attributes! - end - - private - - def request_data - @request_data ||= begin - @params.to_unsafe_h - rescue NoMethodError - @params - end - end - - def validate_request_attributes! - return request_data unless request_attributes_exist? - - # Validate request attributes with orchestrator: - orchestrator_class = Class.new(Treaty::Entity::Attribute::Validation::Orchestrator::Base) do - define_method(:collection_of_attributes) do - @version_factory.request_factory.collection_of_attributes - end - end - - orchestrator_class.validate!( - version_factory: @version_factory, - data: request_data - ) - end - - def request_attributes_exist? - return false if @version_factory.request_factory&.collection_of_attributes&.empty? - - @version_factory.request_factory.collection_of_attributes.exists? - end - end - end -end diff --git a/lib/treaty/response/attribute/attribute.rb b/lib/treaty/response/attribute/attribute.rb deleted file mode 100644 index 0fb0052..0000000 --- a/lib/treaty/response/attribute/attribute.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Response - module Attribute - # Response-specific attribute that defaults to required: false - class Attribute < Treaty::Entity::Attribute::Base - private - - def apply_defaults! - # For response: optional by default (false). - # message: nil means use I18n default message from validators - @options[:required] ||= { is: false, message: nil } - end - - def process_nested_attributes(&block) - return unless object_or_array? - - builder = Builder.new(collection_of_attributes, @nesting_level + 1) - builder.instance_eval(&block) - end - end - end - end -end diff --git a/lib/treaty/response/attribute/builder.rb b/lib/treaty/response/attribute/builder.rb deleted file mode 100644 index 02b9244..0000000 --- a/lib/treaty/response/attribute/builder.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Response - module Attribute - # Response-specific attribute builder - class Builder < Treaty::Entity::Attribute::Builder::Base - private - - def create_attribute(name, type, *helpers, nesting_level:, **options, &block) - Attribute.new( - name, - type, - *helpers, - nesting_level:, - **options, - &block - ) - end - - # Deep copies an attribute with adjusted nesting level for Response context. - # - # @param source_attribute [Treaty::Entity::Attribute::Base] Attribute to copy - # @param new_nesting_level [Integer] New nesting level - # @return [Response::Attribute::Attribute] Copied attribute - def deep_copy_attribute(source_attribute, new_nesting_level) # rubocop:disable Metrics/MethodLength - copied = Attribute.new( - source_attribute.name, - source_attribute.type, - nesting_level: new_nesting_level, - **deep_copy_options(source_attribute.options) - ) - - return copied unless source_attribute.nested? - - source_attribute.collection_of_attributes.each do |nested_source| - nested_copied = deep_copy_attribute(nested_source, new_nesting_level + 1) - copied.collection_of_attributes << nested_copied - end - - copied - end - end - end - end -end diff --git a/lib/treaty/response/entity.rb b/lib/treaty/response/entity.rb deleted file mode 100644 index 5a5bcec..0000000 --- a/lib/treaty/response/entity.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Response - # Entity class for response definitions. - # Attributes are optional by default. - # - # This class is used internally when defining response blocks. - # When you write a response block, Treaty creates an anonymous - # class based on Response::Entity. - class Entity - include Treaty::Entity::Attribute::DSL - - class << self - private - - # Creates a Response::Attribute::Attribute for this Response::Entity class - # - # @return [Response::Attribute::Attribute] Created attribute instance - def create_attribute(name, type, *helpers, nesting_level:, **options, &block) - Attribute::Attribute.new( - name, - type, - *helpers, - nesting_level:, - **options, - &block - ) - end - end - end - end -end diff --git a/lib/treaty/response/factory.rb b/lib/treaty/response/factory.rb deleted file mode 100644 index c7a0970..0000000 --- a/lib/treaty/response/factory.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Response - # Factory for creating response definitions. - # - # Supports two modes: - # 1. Block mode: Creates an anonymous Response::Entity class with the block - # 2. Entity mode: Uses a provided Entity class directly - # - # ## Block Mode - # - # ```ruby - # response 200 do - # object :post do - # string :id - # end - # end - # ``` - # - # ## Entity Mode - # - # ```ruby - # response 200, Posts::Create::ResponseEntity - # ``` - class Factory - attr_reader :status - - def initialize(status) - @status = status - end - - # Uses a provided Entity class - # - # @param entity_class [Class] Entity class to use - # @return [void] - # @raise [Treaty::Exceptions::Validation] if entity_class is not a valid Treaty::Entity::Base subclass - def use_entity(entity_class) - validate_entity_class!(entity_class) - @entity_class = entity_class - end - - # Returns collection of attributes from the entity class - # - # @return [Collection] Collection of attributes - def collection_of_attributes - return Treaty::Entity::Attribute::Collection.new if @entity_class.nil? - - @entity_class.collection_of_attributes - end - - # Handles DSL methods for defining attributes - # - # This allows the factory to be used with method_missing - # for backwards compatibility with direct method calls. - # Creates an anonymous Response::Entity class on first use. - def method_missing(type, *helpers, **options, &block) - # If no entity class yet, create one - @entity_class ||= Class.new(Entity) - - # Call the method on the entity class - @entity_class.public_send(type, *helpers, **options, &block) - end - - def respond_to_missing?(name, *) - super - end - - private - - # Validates that the provided entity_class is a valid Treaty::Entity::Base subclass - # - # @param entity_class [Class] Entity class to validate - # @raise [Treaty::Exceptions::Validation] if entity_class is not a valid Treaty::Entity::Base subclass - def validate_entity_class!(entity_class) - return if entity_class.is_a?(Class) && entity_class < Treaty::Entity::Base - - raise Treaty::Exceptions::Validation, - I18n.t( - "treaty.response.factory.invalid_entity_class", - type: entity_class.class, - value: entity_class - ) - end - end - end -end diff --git a/lib/treaty/response/validator.rb b/lib/treaty/response/validator.rb deleted file mode 100644 index 45e32ac..0000000 --- a/lib/treaty/response/validator.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Response - # Validator for response data - class Validator - class << self - # Validates response data against the response definition - # - # @param response_data [Hash] Response data to validate - # @param version_factory [Versions::Factory] Version factory with response definition - # @return [Hash] Validated and transformed response data - def validate!(version_factory:, response_data: {}) - new(version_factory:, response_data:).validate! - end - end - - def initialize(version_factory:, response_data: {}) - @version_factory = version_factory - @response_data = response_data - end - - def validate! - validate_response_attributes! - end - - private - - def validate_response_attributes! - return @response_data unless response_attributes_exist? - - # Create orchestrator for response validation - # Orchestrator filters data by attributes and performs transformation - orchestrator_class = Class.new(Treaty::Entity::Attribute::Validation::Orchestrator::Base) do - define_method(:collection_of_attributes) do - @version_factory.response_factory.collection_of_attributes - end - end - - orchestrator_class.validate!( - version_factory: @version_factory, - data: @response_data - ) - end - - def response_attributes_exist? - return false if @version_factory.response_factory&.collection_of_attributes&.empty? - - @version_factory.response_factory.collection_of_attributes.exists? - end - end - end -end diff --git a/lib/treaty/result.rb b/lib/treaty/result.rb deleted file mode 100644 index 465ec1b..0000000 --- a/lib/treaty/result.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module Treaty - class Result - attr_reader :data, :status, :version - - def initialize(data:, status:, version:) - @data = data - @status = status - @version = version - end - - def inspect - "#<#{self.class.name} #{draw_result}>" - end - - private - - def draw_result - "@data=#{@data.inspect}, @status=#{@status.inspect}, @version=#{@version.inspect}" - end - end -end diff --git a/lib/treaty/versions/collection.rb b/lib/treaty/versions/collection.rb deleted file mode 100644 index 8ed0b10..0000000 --- a/lib/treaty/versions/collection.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Versions - class Collection - extend Forwardable - - def_delegators :@collection, :<<, :map, :find - - def initialize(collection = Set.new) - @collection = collection - end - end - end -end diff --git a/lib/treaty/versions/dsl.rb b/lib/treaty/versions/dsl.rb deleted file mode 100644 index 621e376..0000000 --- a/lib/treaty/versions/dsl.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Versions - module DSL - def self.included(base) - base.extend(ClassMethods) - base.include(Workspace) - end - - module ClassMethods - private - - def version(version, default: false, &block) - @version_factory = Factory.new(version:, default:) - - @version_factory.instance_eval(&block) - @version_factory.validate_after_block! - - validate_multiple_defaults! if @version_factory.default_result == true - - collection_of_versions << @version_factory - - @version_factory = nil - end - - def collection_of_versions - @collection_of_versions ||= Collection.new - end - - def validate_multiple_defaults! - existing_defaults = collection_of_versions.map(&:default_result).count(true) - - return if existing_defaults.zero? - - raise Treaty::Exceptions::VersionMultipleDefaults, - I18n.t("treaty.versioning.factory.multiple_defaults") - end - end - end - end -end diff --git a/lib/treaty/versions/execution/request.rb b/lib/treaty/versions/execution/request.rb deleted file mode 100644 index 85e3e27..0000000 --- a/lib/treaty/versions/execution/request.rb +++ /dev/null @@ -1,177 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Versions - module Execution - class Request # rubocop:disable Metrics/ClassLength - def self.execute!(...) - new(...).execute! - end - - def initialize(version_factory:, validated_params:, inventory: nil, context: nil) - @inventory = inventory - @context = context - @version_factory = version_factory - @validated_params = validated_params - end - - def execute! - raise_executor_missing_error! if @version_factory.executor.nil? - - extract_data_from_result - end - - private - - def extract_data_from_result - return execution_result if executor.is_a?(Proc) - return execution_result.data if execution_result.respond_to?(:data) - - execution_result - end - - ######################################################################## - - def execution_result - @execution_result ||= - if executor.is_a?(Proc) - execute_proc - elsif servactory_service? - execute_servactory - else - execute_regular_class - end - end - - ######################################################################## - - def executor - @executor ||= resolve_executor(@version_factory.executor.executor) - end - - ######################################################################## - - def resolve_executor(executor) # rubocop:disable Metrics/MethodLength - return executor if executor.is_a?(Proc) || executor.is_a?(Class) - - if executor.is_a?(String) || executor.is_a?(Symbol) - string_executor = executor.to_s - - if string_executor.empty? - raise Treaty::Exceptions::Execution, - I18n.t("treaty.execution.executor_empty") - end - - constant_name = normalize_constant_name(executor) - - begin - constant_name.constantize - rescue NameError - raise Treaty::Exceptions::Execution, - I18n.t("treaty.execution.executor_not_found", class_name: constant_name) - end - else - raise Treaty::Exceptions::Execution, - I18n.t("treaty.execution.executor_invalid_type", type: executor.class) - end - end - - ######################################################################## - - def normalize_constant_name(name) - string = name.to_s - - return string if string.include?("::") - return string.split("/").map(&:camelize).join("::") if string.include?("/") - - string - end - - ######################################################################## - ######################################################################## - ######################################################################## - - # Creates inventory wrapper with lazy evaluation - # - # @return [Treaty::Executor::Inventory] Inventory wrapper with method-based access - def evaluated_inventory - @evaluated_inventory ||= Treaty::Executor::Inventory.new(@inventory, @context) - end - - ######################################################################## - - def execute_proc - # For Proc executors, pass inventory if collection exists - executor.call(**build_call_params) - rescue StandardError => e - raise Treaty::Exceptions::Execution, - I18n.t("treaty.execution.proc_error", message: e.message) - end - - def execute_servactory # rubocop:disable Metrics/MethodLength - # For Servactory services, pass inventory if collection exists - executor.call!(**build_call_params) - rescue Servactory::Exceptions::Input => e - raise Treaty::Exceptions::Execution, - I18n.t("treaty.execution.servactory_input_error", message: e.message) - rescue Servactory::Exceptions::Internal => e - raise Treaty::Exceptions::Execution, - I18n.t("treaty.execution.servactory_internal_error", message: e.message) - rescue Servactory::Exceptions::Output => e - raise Treaty::Exceptions::Execution, - I18n.t("treaty.execution.servactory_output_error", message: e.message) - rescue Servactory::Exceptions::Failure => e - raise Treaty::Exceptions::Execution, - I18n.t("treaty.execution.servactory_failure_error", message: e.message) - end - - def execute_regular_class # rubocop:disable Metrics/MethodLength - method_name = @version_factory.executor.method - - unless executor.respond_to?(method_name) - raise Treaty::Exceptions::Execution, - I18n.t( - "treaty.execution.method_not_found", - method: method_name, - class_name: executor - ) - end - - # For regular classes, pass inventory if collection exists - executor.public_send(method_name, **build_call_params) - rescue StandardError => e - raise Treaty::Exceptions::Execution, - I18n.t("treaty.execution.regular_service_error", message: e.message) - end - - ######################################################################## - ######################################################################## - ######################################################################## - - # Builds call parameters hash with inventory if it exists - # - # @return [Hash] Parameters hash with :params and optionally :inventory - def build_call_params - if @inventory&.exists? - { params: @validated_params, inventory: evaluated_inventory } - else - { params: @validated_params } - end - end - - def raise_executor_missing_error! - raise Treaty::Exceptions::Execution, - I18n.t( - "treaty.execution.executor_missing", - version: @version_factory.version - ) - end - - def servactory_service? - executor.respond_to?(:servactory?) && - executor.servactory? - end - end - end - end -end diff --git a/lib/treaty/versions/executor.rb b/lib/treaty/versions/executor.rb deleted file mode 100644 index 3de0b00..0000000 --- a/lib/treaty/versions/executor.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Versions - class Executor - attr_reader :executor, :method - - def initialize(executor, method) - @executor = executor - @method = method - end - end - end -end diff --git a/lib/treaty/versions/factory.rb b/lib/treaty/versions/factory.rb deleted file mode 100644 index d0daac8..0000000 --- a/lib/treaty/versions/factory.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Versions - class Factory - attr_reader :version, - :default_result, - :summary_text, - :deprecated_result, - :executor, - :request_factory, - :response_factory - - def initialize(version:, default:) - @version = Semantic.new(version) - @default_result = default.is_a?(Proc) ? default.call : default - @summary_text = nil - @deprecated_result = false - @executor = nil - - validate! - end - - def validate! - validate_default_option! - end - - def validate_after_block! - validate_default_deprecated_conflict! - end - - def summary(text) - @summary_text = text - end - - def deprecated(condition = nil) - result = - if condition.is_a?(Proc) - condition.call - elsif condition.is_a?(TrueClass) || condition.is_a?(FalseClass) - condition - else - yield - end - - @deprecated_result = result - end - - def request(entity_class = nil, &block) - @request_factory ||= Request::Factory.new - - if entity_class.present? - @request_factory.use_entity(entity_class) - elsif block_given? - @request_factory.instance_eval(&block) - end - end - - def response(status, entity_class = nil, &block) - @response_factory ||= Response::Factory.new(status) - - if entity_class.present? - @response_factory.use_entity(entity_class) - elsif block_given? - @response_factory.instance_eval(&block) - end - end - - def delegate_to(executor, method = :call) - @executor = Executor.new(executor, method) - end - - ########################################################################## - - private - - def validate_default_option! - if @default_result.is_a?(TrueClass) || @default_result.is_a?(FalseClass) || @default_result.is_a?(Proc) - return @default_result - end - - raise Treaty::Exceptions::Validation, - I18n.t( - "treaty.versioning.factory.invalid_default_option", - type: @default_result.class - ) - end - - def validate_default_deprecated_conflict! - return unless @default_result == true - return unless @deprecated_result == true - - raise Treaty::Exceptions::VersionDefaultDeprecatedConflict, - I18n.t( - "treaty.versioning.factory.default_deprecated_conflict", - version: @version.version - ) - end - - ########################################################################## - - def method_missing(name, *, &_block) - raise Treaty::Exceptions::MethodName, - I18n.t("treaty.versioning.factory.unknown_method", method: name) - end - - def respond_to_missing?(name, *) - super - end - end - end -end diff --git a/lib/treaty/versions/resolver.rb b/lib/treaty/versions/resolver.rb deleted file mode 100644 index ad4e90b..0000000 --- a/lib/treaty/versions/resolver.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Versions - class Resolver - def self.resolve!(...) - new(...).resolve! - end - - def initialize(specified_version:, collection_of_versions:) - @specified_version = specified_version - @collection_of_versions = collection_of_versions - end - - def resolve! - determined_factory = - if specified_version_blank? - default_version_factory || raise_specified_version_not_found! - else - version_factory || raise_version_not_found! - end - - raise_version_deprecated! if determined_factory.deprecated_result - - determined_factory - end - - private - - def version_factory - @version_factory ||= - @collection_of_versions.find do |factory| - factory.version.version == @specified_version - end - end - - def default_version_factory - @default_version_factory ||= - @collection_of_versions.find(&:default_result) - end - - def specified_version_blank? - @specified_version.to_s.strip.empty? - end - - ########################################################################## - - def raise_specified_version_not_found! - raise Treaty::Exceptions::SpecifiedVersionNotFound, - I18n.t("treaty.versioning.resolver.specified_version_required") - end - - def raise_version_not_found! - raise Treaty::Exceptions::VersionNotFound, - I18n.t( - "treaty.versioning.resolver.version_not_found", - version: @specified_version - ) - end - - def raise_version_deprecated! - raise Treaty::Exceptions::Deprecated, - I18n.t( - "treaty.versioning.resolver.version_deprecated", - version: @specified_version - ) - end - end - end -end diff --git a/lib/treaty/versions/semantic.rb b/lib/treaty/versions/semantic.rb deleted file mode 100644 index 66d6061..0000000 --- a/lib/treaty/versions/semantic.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Versions - class Semantic - attr_reader :version - - def initialize(version) - version = - if version.is_a?(Array) - version.join(".") - # elsif version.is_a?(Integer) - # version.to_s - else - version # rubocop:disable Style/RedundantSelfAssignmentBranch - end - - @version = Gem::Version.new(version) - end - end - end -end diff --git a/lib/treaty/versions/workspace.rb b/lib/treaty/versions/workspace.rb deleted file mode 100644 index 9031165..0000000 --- a/lib/treaty/versions/workspace.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module Treaty - module Versions - module Workspace - private - - def call!(context:, inventory:, version:, params:, **) # rubocop:disable Metrics/MethodLength - super - - version_factory = Resolver.resolve!( - specified_version: version, - collection_of_versions: @collection_of_versions - ) - - validated_params = Request::Validator.validate!( - params:, - version_factory: - ) - - executor_result = Execution::Request.execute!( - context:, - inventory:, - version_factory:, - validated_params: - ) - - validated_response = Response::Validator.validate!( - version_factory:, - response_data: executor_result - ) - - status = version_factory.response_factory&.status || 200 - - Treaty::Result.new( - data: validated_response, - status:, - version: version_factory.version.version - ) - end - end - end -end diff --git a/spec/sandbox/app/services/posts/stable/create_service.rb b/spec/sandbox/app/services/posts/stable/create_service.rb index 7269c4d..54653e7 100644 --- a/spec/sandbox/app/services/posts/stable/create_service.rb +++ b/spec/sandbox/app/services/posts/stable/create_service.rb @@ -3,7 +3,7 @@ module Posts module Stable class CreateService < ApplicationService::Base - input :inventory, type: Treaty::Executor::Inventory, required: false + input :inventory, type: Treaty::Action::Executor::Inventory, required: false input :params, type: Hash output :data, type: Hash diff --git a/spec/sandbox/app/services/posts/stable/index_service.rb b/spec/sandbox/app/services/posts/stable/index_service.rb index ca5d483..4c99761 100644 --- a/spec/sandbox/app/services/posts/stable/index_service.rb +++ b/spec/sandbox/app/services/posts/stable/index_service.rb @@ -3,7 +3,7 @@ module Posts module Stable class IndexService < ApplicationService::Base - input :inventory, type: Treaty::Executor::Inventory, required: false + input :inventory, type: Treaty::Action::Executor::Inventory, required: false input :params, type: Hash, required: false, default: {} output :data, type: Hash diff --git a/spec/sandbox/app/services/posts/v1/create_service.rb b/spec/sandbox/app/services/posts/v1/create_service.rb index 5510ad6..ef44be1 100644 --- a/spec/sandbox/app/services/posts/v1/create_service.rb +++ b/spec/sandbox/app/services/posts/v1/create_service.rb @@ -3,7 +3,7 @@ module Posts module V1 class CreateService < ApplicationService::Base - input :inventory, type: Treaty::Executor::Inventory, required: false + input :inventory, type: Treaty::Action::Executor::Inventory, required: false input :params, type: Hash, required: false, default: {} output :data, type: Hash diff --git a/spec/sandbox/app/services/posts/v1/index_service.rb b/spec/sandbox/app/services/posts/v1/index_service.rb index b68a064..34e6d66 100644 --- a/spec/sandbox/app/services/posts/v1/index_service.rb +++ b/spec/sandbox/app/services/posts/v1/index_service.rb @@ -3,7 +3,7 @@ module Posts module V1 class IndexService < ApplicationService::Base - input :inventory, type: Treaty::Executor::Inventory, required: false + input :inventory, type: Treaty::Action::Executor::Inventory, required: false input :params, type: Hash, required: false, default: {} output :data, type: Hash diff --git a/spec/sandbox/app/treaties/application_treaty.rb b/spec/sandbox/app/treaties/application_treaty.rb index 7713d86..7627a4c 100644 --- a/spec/sandbox/app/treaties/application_treaty.rb +++ b/spec/sandbox/app/treaties/application_treaty.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true -class ApplicationTreaty < Treaty::Base +class ApplicationTreaty < Treaty::Action::Base end diff --git a/spec/support/shared_examples/check_class_info.rb b/spec/support/shared_examples/check_class_info.rb index 9fb64c1..f1dfd29 100644 --- a/spec/support/shared_examples/check_class_info.rb +++ b/spec/support/shared_examples/check_class_info.rb @@ -3,7 +3,7 @@ RSpec.shared_examples "check treaty class info" do |versions:| it "returns expected information about class", :aggregate_failures do expect(described_class.respond_to?(:info)).to be(true) - expect(described_class.info).to be_a(Treaty::Info::Rest::Result) + expect(described_class.info).to be_a(Treaty::Action::Info::Result) expect(described_class.info.respond_to?(:versions)).to be(true) expect(described_class.info.versions).to match(versions) diff --git a/spec/treaties/languages/index_treaty_spec.rb b/spec/treaties/languages/index_treaty_spec.rb index 252dec7..6e83553 100644 --- a/spec/treaties/languages/index_treaty_spec.rb +++ b/spec/treaties/languages/index_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Languages::IndexTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/languages/show_treaty_spec.rb b/spec/treaties/languages/show_treaty_spec.rb index 57a26c3..6715815 100644 --- a/spec/treaties/languages/show_treaty_spec.rb +++ b/spec/treaties/languages/show_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Languages::ShowTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/movies/index_treaty_spec.rb b/spec/treaties/movies/index_treaty_spec.rb index 882c709..8d5cc9f 100644 --- a/spec/treaties/movies/index_treaty_spec.rb +++ b/spec/treaties/movies/index_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Movies::IndexTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/movies/show_treaty_spec.rb b/spec/treaties/movies/show_treaty_spec.rb index bb5deed..bc0fd75 100644 --- a/spec/treaties/movies/show_treaty_spec.rb +++ b/spec/treaties/movies/show_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Movies::ShowTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/posts/create_treaty_spec.rb b/spec/treaties/posts/create_treaty_spec.rb index 6815ae9..9f011fb 100644 --- a/spec/treaties/posts/create_treaty_spec.rb +++ b/spec/treaties/posts/create_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Posts::CreateTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/posts/index_treaty_spec.rb b/spec/treaties/posts/index_treaty_spec.rb index 209b884..782e883 100644 --- a/spec/treaties/posts/index_treaty_spec.rb +++ b/spec/treaties/posts/index_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Posts::IndexTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/showcase/as_treaty_spec.rb b/spec/treaties/showcase/as_treaty_spec.rb index 6743876..37bca25 100644 --- a/spec/treaties/showcase/as_treaty_spec.rb +++ b/spec/treaties/showcase/as_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Showcase::AsTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/showcase/cast_treaty_spec.rb b/spec/treaties/showcase/cast_treaty_spec.rb index 539d41a..840b68d 100644 --- a/spec/treaties/showcase/cast_treaty_spec.rb +++ b/spec/treaties/showcase/cast_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Showcase::CastTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/showcase/computed_treaty_spec.rb b/spec/treaties/showcase/computed_treaty_spec.rb index ced07c9..3489d4d 100644 --- a/spec/treaties/showcase/computed_treaty_spec.rb +++ b/spec/treaties/showcase/computed_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Showcase::ComputedTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/showcase/default_treaty_spec.rb b/spec/treaties/showcase/default_treaty_spec.rb index 00228c9..7bb21dd 100644 --- a/spec/treaties/showcase/default_treaty_spec.rb +++ b/spec/treaties/showcase/default_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Showcase::DefaultTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/showcase/format_treaty_spec.rb b/spec/treaties/showcase/format_treaty_spec.rb index d4e0e30..db72b02 100644 --- a/spec/treaties/showcase/format_treaty_spec.rb +++ b/spec/treaties/showcase/format_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Showcase::FormatTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/showcase/if_treaty_spec.rb b/spec/treaties/showcase/if_treaty_spec.rb index 062812e..acab5cb 100644 --- a/spec/treaties/showcase/if_treaty_spec.rb +++ b/spec/treaties/showcase/if_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Showcase::IfTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/showcase/inclusion_treaty_spec.rb b/spec/treaties/showcase/inclusion_treaty_spec.rb index 4d3ed63..139cd4f 100644 --- a/spec/treaties/showcase/inclusion_treaty_spec.rb +++ b/spec/treaties/showcase/inclusion_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Showcase::InclusionTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/showcase/required_treaty_spec.rb b/spec/treaties/showcase/required_treaty_spec.rb index 3743dd3..8bae98d 100644 --- a/spec/treaties/showcase/required_treaty_spec.rb +++ b/spec/treaties/showcase/required_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Showcase::RequiredTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/showcase/transform_treaty_spec.rb b/spec/treaties/showcase/transform_treaty_spec.rb index ca088e7..d6b196a 100644 --- a/spec/treaties/showcase/transform_treaty_spec.rb +++ b/spec/treaties/showcase/transform_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Showcase::TransformTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/showcase/unless_treaty_spec.rb b/spec/treaties/showcase/unless_treaty_spec.rb index c89f328..4cce795 100644 --- a/spec/treaties/showcase/unless_treaty_spec.rb +++ b/spec/treaties/showcase/unless_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Showcase::UnlessTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaties/users/create_treaty_spec.rb b/spec/treaties/users/create_treaty_spec.rb index 2a3ecb7..aa904d4 100644 --- a/spec/treaties/users/create_treaty_spec.rb +++ b/spec/treaties/users/create_treaty_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Users::CreateTreaty do subject(:perform) { described_class.call!(inventory:, version:, params:) } - let(:inventory_collection) { Treaty::Inventory::Collection.new } + let(:inventory_collection) { Treaty::Action::Inventory::Collection.new } let(:context) { instance_double(ApplicationController) } let(:inventory) do - Treaty::Executor::Inventory.new(inventory_collection, context).tap do |inventory| + Treaty::Action::Executor::Inventory.new(inventory_collection, context).tap do |inventory| allow(inventory).to receive(:exists?).and_return(inventory_collection.exists?) end end diff --git a/spec/treaty/versions/execution/request_spec.rb b/spec/treaty/action/versions/execution/request_spec.rb similarity index 94% rename from spec/treaty/versions/execution/request_spec.rb rename to spec/treaty/action/versions/execution/request_spec.rb index 01e633e..e699023 100644 --- a/spec/treaty/versions/execution/request_spec.rb +++ b/spec/treaty/action/versions/execution/request_spec.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -RSpec.describe Treaty::Versions::Execution::Request do +RSpec.describe Treaty::Action::Versions::Execution::Request do describe "#execute!" do let(:version_factory) do instance_double( - Treaty::Versions::Factory, + Treaty::Action::Versions::Factory, version: 3, executor: executor_instance ) @@ -15,7 +15,7 @@ context "when executor is defined as a path-style string" do let(:executor_instance) do instance_double( - Treaty::Versions::Executor, + Treaty::Action::Versions::Executor, executor: "posts/stable/create_service", method: :call! ) @@ -52,7 +52,7 @@ context "when executor is defined as a string with underscores" do let(:executor_instance) do instance_double( - Treaty::Versions::Executor, + Treaty::Action::Versions::Executor, executor: "posts/v1/create_service", method: :call! ) @@ -85,7 +85,7 @@ context "when executor is defined as a traditional constant string" do let(:executor_instance) do instance_double( - Treaty::Versions::Executor, + Treaty::Action::Versions::Executor, executor: "Posts::Stable::CreateService", method: :call! ) @@ -118,7 +118,7 @@ context "when executor is defined as a Class" do let(:executor_instance) do instance_double( - Treaty::Versions::Executor, + Treaty::Action::Versions::Executor, executor: Posts::Stable::CreateService, method: :call! ) @@ -151,7 +151,7 @@ context "when executor is defined as a Proc" do let(:executor_instance) do instance_double( - Treaty::Versions::Executor, + Treaty::Action::Versions::Executor, executor: executor_proc, method: :call ) @@ -175,7 +175,7 @@ context "when executor is a Servactory service" do let(:executor_instance) do instance_double( - Treaty::Versions::Executor, + Treaty::Action::Versions::Executor, executor: Posts::Stable::CreateService, method: :call! ) @@ -209,7 +209,7 @@ context "when executor is an empty string" do let(:executor_instance) do instance_double( - Treaty::Versions::Executor, + Treaty::Action::Versions::Executor, executor: "", method: :call ) @@ -230,7 +230,7 @@ context "when executor path does not exist" do let(:executor_instance) do instance_double( - Treaty::Versions::Executor, + Treaty::Action::Versions::Executor, executor: "nonexistent/path/service", method: :call ) @@ -267,7 +267,7 @@ describe "#normalize_constant_name" do let(:version_factory) do instance_double( - Treaty::Versions::Factory, + Treaty::Action::Versions::Factory, version: 3, executor: nil )