Class: Apiwork::API::Base
- Inherits:
-
Object
- Object
- Apiwork::API::Base
- Defined in:
- lib/apiwork/api/base.rb
Overview
Base class for API definitions.
Created via define. Configure resources, types, enums, adapters, and exports. Each API is mounted at a unique path.
Constant Summary collapse
- VALID_FORMATS =
%i[keep camel pascal kebab underscore].freeze
Class Attribute Summary collapse
-
.base_path ⇒ String
readonly
The base path for this API.
- .enum_registry ⇒ Object readonly
- .explorer_config ⇒ Object readonly
- .export_configs ⇒ Object readonly
- .representation_registry ⇒ Object readonly
- .root_resource ⇒ Object readonly
- .type_registry ⇒ Object readonly
Class Method Summary collapse
-
.adapter(name = nil) {|config| ... } ⇒ Adapter::Base, void
Sets or configures the adapter for this API.
- .adapter_class ⇒ Object
- .adapter_config ⇒ Object
-
.concern(name) {|resource| ... } ⇒ void
Defines a reusable concern for resources.
- .ensure_all_contracts_built! ⇒ Object
- .ensure_contract_built!(contract_class) ⇒ Object
- .ensure_pre_pass_complete! ⇒ Object
-
.enum(name, deprecated: false, description: nil, example: nil, values: nil) ⇒ void
Defines a reusable enumeration type.
- .enum?(name, scope: nil) ⇒ Boolean
- .enum_values(name, scope: nil) ⇒ Object
-
.explorer {|explorer| ... } ⇒ void
Configures the explorer for this API.
-
.export(name) {|export| ... } ⇒ void
Enables an export for this API.
-
.fingerprint ⇒ String
The fingerprint for this API.
-
.fragment(name) {|object| ... } ⇒ void
Defines a fragment type for composition.
-
.info {|info| ... } ⇒ Info?
The info for this API.
- .introspect(locale: nil) ⇒ Object
- .introspect_contract(contract_class, expand:, locale:) ⇒ Object
-
.key_format(format = nil) ⇒ Symbol?
Configures key transformation for this API.
- .locale_key ⇒ Object
-
.locales(*locale_keys) ⇒ Array<Symbol>
Supported locales for this API.
- .mount(base_path) ⇒ Object
- .namespaces ⇒ Object
- .normalize_key(key) ⇒ Object
- .normalize_request(request) ⇒ Object
-
.object(name, deprecated: false, description: nil, example: nil) {|object| ... } ⇒ void
Defines a reusable object type.
-
.path_format(format = nil) ⇒ Symbol?
Configures URL path transformation for this API.
- .prepare_error_response(response) ⇒ Object
- .prepare_request(request) ⇒ Object
- .prepare_response(response) ⇒ Object
-
.raises(*error_code_keys) ⇒ Array<Symbol>
API-wide error codes.
- .register_enum(name, deprecated: false, description: nil, example: nil, scope: nil, values: nil) ⇒ Object
- .register_fragment(name, scope: nil, &block) ⇒ Object
- .register_object(name, deprecated: false, description: nil, example: nil, scope: nil, &block) ⇒ Object
- .register_union(name, deprecated: false, description: nil, discriminator: nil, example: nil, scope: nil, &block) ⇒ Object
- .reset_contracts! ⇒ Object
-
.resource(name, concerns: nil, constraints: nil, contract: nil, controller: nil, defaults: nil, except: nil, only: nil, param: nil, path: nil) {|resource| ... } ⇒ void
Defines a singular resource (no index action, no :id in URL).
-
.resources(name, concerns: nil, constraints: nil, contract: nil, controller: nil, defaults: nil, except: nil, only: nil, param: nil, path: nil) {|resource| ... } ⇒ void
Defines a RESTful resource with standard CRUD actions.
- .scoped_enum_name(scope, name) ⇒ Object
- .scoped_type_name(scope, name) ⇒ Object
- .transform_key(key) ⇒ Object
- .transform_path(path) ⇒ Object
- .translate(*segments, default: nil) ⇒ Object
- .type?(name, scope: nil) ⇒ Boolean
- .type_definition(name, scope: nil) ⇒ Object
-
.union(name, deprecated: false, description: nil, discriminator: nil, example: nil) {|union| ... } ⇒ void
Defines a discriminated union type.
-
.with_options(options = {}) {|resource| ... } ⇒ void
Applies options to all nested resource definitions.
Class Attribute Details
.base_path ⇒ String (readonly)
The base path for this API.
28 29 30 |
# File 'lib/apiwork/api/base.rb', line 28 def base_path @base_path end |
.enum_registry ⇒ Object (readonly)
28 29 30 |
# File 'lib/apiwork/api/base.rb', line 28 def enum_registry @enum_registry end |
.explorer_config ⇒ Object (readonly)
28 29 30 |
# File 'lib/apiwork/api/base.rb', line 28 def explorer_config @explorer_config end |
.export_configs ⇒ Object (readonly)
28 29 30 |
# File 'lib/apiwork/api/base.rb', line 28 def export_configs @export_configs end |
.representation_registry ⇒ Object (readonly)
28 29 30 |
# File 'lib/apiwork/api/base.rb', line 28 def representation_registry @representation_registry end |
.root_resource ⇒ Object (readonly)
28 29 30 |
# File 'lib/apiwork/api/base.rb', line 28 def root_resource @root_resource end |
.type_registry ⇒ Object (readonly)
28 29 30 |
# File 'lib/apiwork/api/base.rb', line 28 def type_registry @type_registry end |
Class Method Details
.adapter(name = nil) {|config| ... } ⇒ Adapter::Base, void
Sets or configures the adapter for this API.
Without arguments, returns the adapter instance. With a block, configures the current adapter. Without a name, the built-in ‘:standard` adapter is used.
Custom adapters must be registered via Apiwork::Adapter.register and referenced by their ‘adapter_name`.
209 210 211 212 213 214 215 216 217 218 |
# File 'lib/apiwork/api/base.rb', line 209 def adapter(name = nil, &block) @adapter_name = name if name.is_a?(Symbol) if block block.arity.positive? ? yield(adapter_config) : adapter_config.instance_eval(&block) return end @adapter ||= adapter_class.new end |
.adapter_class ⇒ Object
662 663 664 |
# File 'lib/apiwork/api/base.rb', line 662 def adapter_class Adapter.find!(@adapter_name || :standard) end |
.adapter_config ⇒ Object
666 667 668 |
# File 'lib/apiwork/api/base.rb', line 666 def adapter_config @adapter_config ||= Configuration.new(adapter_class) end |
.concern(name) {|resource| ... } ⇒ void
This method returns an undefined value.
Defines a reusable concern for resources.
Concerns are reusable blocks of resource configuration that can be included in multiple resources via the ‘concerns` option.
568 569 570 |
# File 'lib/apiwork/api/base.rb', line 568 def concern(name, &block) @root_resource.concern(name, &block) end |
.ensure_all_contracts_built! ⇒ Object
838 839 840 841 842 843 844 |
# File 'lib/apiwork/api/base.rb', line 838 def ensure_all_contracts_built! ensure_pre_pass_complete! @root_resource.each_resource do |resource| build_contracts_for_resource(resource) end end |
.ensure_contract_built!(contract_class) ⇒ Object
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 |
# File 'lib/apiwork/api/base.rb', line 814 def ensure_contract_built!(contract_class) return if @built_contracts.include?(contract_class) ensure_pre_pass_complete! representation_class = contract_class.representation_class return unless representation_class @built_contracts.add(contract_class) resource = @root_resource.find_resource { |resource| resource.resolve_contract_class == contract_class } resource_actions = resource ? resource.actions : {} adapter.register_contract(contract_class, representation_class, resource_actions:) end |
.ensure_pre_pass_complete! ⇒ Object
830 831 832 833 834 835 836 |
# File 'lib/apiwork/api/base.rb', line 830 def ensure_pre_pass_complete! return if @pre_pass_complete mark_nested_writable_representations! adapter.register_api(self) @pre_pass_complete = true end |
.enum(name, deprecated: false, description: nil, example: nil, values: nil) ⇒ void
This method returns an undefined value.
Defines a reusable enumeration type.
286 287 288 |
# File 'lib/apiwork/api/base.rb', line 286 def enum(name, deprecated: false, description: nil, example: nil, values: nil) register_enum(name, deprecated:, description:, example:, values:) end |
.enum?(name, scope: nil) ⇒ Boolean
781 782 783 |
# File 'lib/apiwork/api/base.rb', line 781 def enum?(name, scope: nil) enum_registry.exists?(name, scope:) end |
.enum_values(name, scope: nil) ⇒ Object
785 786 787 |
# File 'lib/apiwork/api/base.rb', line 785 def enum_values(name, scope: nil) enum_registry.values(name, scope:) end |
.explorer {|explorer| ... } ⇒ void
This method returns an undefined value.
Configures the explorer for this API.
The explorer is an interactive UI for browsing and testing API endpoints. Requires the ‘apiwork-explorer` gem.
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/apiwork/api/base.rb', line 155 def explorer(&block) unless defined?(Apiwork::Explorer::Engine) raise ConfigurationError, 'explorer requires the apiwork-explorer gem. ' \ "Add it to your Gemfile: gem 'apiwork-explorer'" end unless @explorer_config = Configurable.define do option :mode, default: :auto, enum: %i[auto always never], type: :symbol option :path, default: '/.explorer', type: :string end @explorer_config = Configuration.new() end return @explorer_config unless block block.arity.positive? ? yield(@explorer_config) : @explorer_config.instance_eval(&block) end |
.export(name) {|export| ... } ⇒ void
This method returns an undefined value.
Enables an export for this API.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/apiwork/api/base.rb', line 111 def export(name, &block) unless Export.exists?(name) available = Export.keys.join(', ') raise ConfigurationError, "Unknown export: :#{name}. " \ "Available: #{available}" end unless @export_configs[name] export_class = Export.find!(name) = Configurable.define(extends: export_class) do option :endpoint, type: :hash do option :mode, default: :auto, enum: %i[auto always never], type: :symbol option :path, type: :string end end @export_configs[name] = Configuration.new() end return unless block block.arity.positive? ? yield(@export_configs[name]) : @export_configs[name].instance_eval(&block) end |
.fingerprint ⇒ String
The fingerprint for this API.
A 16-character hex digest derived from the application name and base_path.
606 607 608 609 610 |
# File 'lib/apiwork/api/base.rb', line 606 def fingerprint @fingerprint ||= Digest::SHA256.hexdigest( [Rails.application.class.module_parent_name, base_path].join(':'), )[0, 16] end |
.fragment(name) {|object| ... } ⇒ void
This method returns an undefined value.
Defines a fragment type for composition.
Fragments are only available for merging into other types and never appear as standalone types. Use fragments to define reusable field groups.
265 266 267 |
# File 'lib/apiwork/api/base.rb', line 265 def fragment(name, &block) register_fragment(name, &block) end |
.info {|info| ... } ⇒ Info?
The info for this API.
392 393 394 395 396 397 398 |
# File 'lib/apiwork/api/base.rb', line 392 def info(&block) return @info unless block @info = Info.new block.arity.positive? ? yield(@info) : @info.instance_eval(&block) @info end |
.introspect(locale: nil) ⇒ Object
797 798 799 800 |
# File 'lib/apiwork/api/base.rb', line 797 def introspect(locale: nil) ensure_all_contracts_built! @introspect_cache[locale] ||= Introspection.api(self, locale:) end |
.introspect_contract(contract_class, expand:, locale:) ⇒ Object
802 803 804 805 806 |
# File 'lib/apiwork/api/base.rb', line 802 def introspect_contract(contract_class, expand:, locale:) ensure_all_contracts_built! cache_key = [contract_class, locale, ] @introspect_contract_cache[cache_key] ||= Introspection.contract(contract_class, expand:, locale:) end |
.key_format(format = nil) ⇒ Symbol?
Configures key transformation for this API.
Transforms JSON keys in request bodies, response bodies, and query parameters. Incoming requests are normalized to underscore internally, so controllers always receive ‘params` regardless of format.
With ‘:camel`, `user_name` becomes `userName`. With `:pascal`, `user_name` becomes `UserName`. With `:kebab`, `user_name` becomes `user-name`.
57 58 59 60 61 62 63 |
# File 'lib/apiwork/api/base.rb', line 57 def key_format(format = nil) return @key_format if format.nil? raise ConfigurationError, "key_format must be one of #{VALID_FORMATS}" unless VALID_FORMATS.include?(format) @key_format = format end |
.locale_key ⇒ Object
670 671 672 |
# File 'lib/apiwork/api/base.rb', line 670 def locale_key @locale_key ||= base_path.delete_prefix('/') end |
.locales(*locale_keys) ⇒ Array<Symbol>
Supported locales for this API.
Declares which locales this API supports. Used by introspection to validate locale parameters and included in introspection output.
332 333 334 335 336 337 338 339 340 |
# File 'lib/apiwork/api/base.rb', line 332 def locales(*locale_keys) return @locales if locale_keys.empty? locale_keys = locale_keys.flatten.uniq locale_keys.each do |locale_key| raise ConfigurationError, "locales must be symbols, got #{locale_key.class}: #{locale_key}" unless locale_key.is_a?(Symbol) end @locales = locale_keys end |
.mount(base_path) ⇒ Object
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 |
# File 'lib/apiwork/api/base.rb', line 678 def mount(base_path) @base_path = base_path @fingerprint = nil @locale_key = nil @namespaces = nil @info = nil @locales = [] @raises = [] @explorer_config = nil @export_configs = {} @adapter_config = nil @root_resource = Resource.new(self) @type_registry = TypeRegistry.new @enum_registry = EnumRegistry.new @representation_registry = RepresentationRegistry.new @built_contracts = Set.new @key_format = :keep @path_format = :keep @introspect_cache = {} @introspect_contract_cache = {} Registry.register(self) end |
.namespaces ⇒ Object
674 675 676 |
# File 'lib/apiwork/api/base.rb', line 674 def namespaces @namespaces ||= extract_namespaces(base_path) end |
.normalize_key(key) ⇒ Object
739 740 741 742 743 744 745 |
# File 'lib/apiwork/api/base.rb', line 739 def normalize_key(key) key_string = key.to_s return key_string if key_string.match?(/\A[A-Z]+\z/) key_string.underscore end |
.normalize_request(request) ⇒ Object
747 748 749 750 751 752 753 |
# File 'lib/apiwork/api/base.rb', line 747 def normalize_request(request) return request if key_format == :keep request.transform do |hash| hash.deep_transform_keys { |key| normalize_key(key).to_sym } end end |
.object(name, deprecated: false, description: nil, example: nil) {|object| ... } ⇒ void
This method returns an undefined value.
Defines a reusable object type.
240 241 242 |
# File 'lib/apiwork/api/base.rb', line 240 def object(name, deprecated: false, description: nil, example: nil, &block) register_object(name, deprecated:, description:, example:, &block) end |
.path_format(format = nil) ⇒ Symbol?
Configures URL path transformation for this API.
Transforms URL path segments: base path, resource paths, action paths, and explicit ‘path:` options. Path parameters like `:id` and `:user_id` are not transformed. Controllers and params remain underscore internally.
With ‘:kebab`, `/api/user_profiles/:id` becomes `/api/user-profiles/:id`. With `:pascal`, `/api/user_profiles/:id` becomes `/api/UserProfiles/:id`.
87 88 89 90 91 92 93 |
# File 'lib/apiwork/api/base.rb', line 87 def path_format(format = nil) return @path_format if format.nil? raise ConfigurationError, "path_format must be one of #{VALID_FORMATS}" unless VALID_FORMATS.include?(format) @path_format = format end |
.prepare_error_response(response) ⇒ Object
766 767 768 769 770 771 |
# File 'lib/apiwork/api/base.rb', line 766 def prepare_error_response(response) result = prepare_response(response) return result if key_format == :keep result.transform { |body| transform_issue_paths(body) } end |
.prepare_request(request) ⇒ Object
755 756 757 |
# File 'lib/apiwork/api/base.rb', line 755 def prepare_request(request) request end |
.prepare_response(response) ⇒ Object
759 760 761 762 763 764 |
# File 'lib/apiwork/api/base.rb', line 759 def prepare_response(response) result = adapter.apply_response_transformers(response) return result if key_format == :keep result.transform { |hash| hash.deep_transform_keys { |key| transform_key(key).to_sym } } end |
.raises(*error_code_keys) ⇒ Array<Symbol>
API-wide error codes.
Included in generated specs (OpenAPI, etc.) as possible error responses.
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 |
# File 'lib/apiwork/api/base.rb', line 355 def raises(*error_code_keys) return @raises if error_code_keys.empty? error_code_keys = error_code_keys.flatten.uniq error_code_keys.each do |error_code_key| unless error_code_key.is_a?(Symbol) hint = error_code_key.is_a?(Integer) ? " Use :#{ErrorCode.key_for_status(error_code_key)} instead." : '' raise ConfigurationError, "raises must be symbols, got #{error_code_key.class}: #{error_code_key}.#{hint}" end next if ErrorCode.exists?(error_code_key) raise ConfigurationError, "Unknown error code :#{error_code_key}. Register it with: " \ "Apiwork::ErrorCode.register :#{error_code_key}, status: <status>" end @raises = error_code_keys end |
.register_enum(name, deprecated: false, description: nil, example: nil, scope: nil, values: nil) ⇒ Object
634 635 636 637 638 639 640 641 642 643 644 645 |
# File 'lib/apiwork/api/base.rb', line 634 def register_enum(name, deprecated: false, description: nil, example: nil, scope: nil, values: nil) raise ConfigurationError, 'Values must be an array' if values && !values.is_a?(Array) enum_registry.register( name, values, deprecated:, description:, example:, scope:, ) end |
.register_fragment(name, scope: nil, &block) ⇒ Object
624 625 626 627 628 629 630 631 632 |
# File 'lib/apiwork/api/base.rb', line 624 def register_fragment(name, scope: nil, &block) type_registry.register( name, scope:, fragment: true, kind: :object, &block ) end |
.register_object(name, deprecated: false, description: nil, example: nil, scope: nil, &block) ⇒ Object
612 613 614 615 616 617 618 619 620 621 622 |
# File 'lib/apiwork/api/base.rb', line 612 def register_object(name, deprecated: false, description: nil, example: nil, scope: nil, &block) type_registry.register( name, deprecated:, description:, example:, scope:, kind: :object, &block ) end |
.register_union(name, deprecated: false, description: nil, discriminator: nil, example: nil, scope: nil, &block) ⇒ Object
647 648 649 650 651 652 653 654 655 656 657 658 659 660 |
# File 'lib/apiwork/api/base.rb', line 647 def register_union(name, deprecated: false, description: nil, discriminator: nil, example: nil, scope: nil, &block) raise ConfigurationError, 'Union requires a block' unless block_given? type_registry.register( name, deprecated:, description:, discriminator:, example:, scope:, kind: :union, &block ) end |
.reset_contracts! ⇒ Object
808 809 810 811 812 |
# File 'lib/apiwork/api/base.rb', line 808 def reset_contracts! @built_contracts = Set.new @introspect_cache = {} @introspect_contract_cache = {} end |
.resource(name, concerns: nil, constraints: nil, contract: nil, controller: nil, defaults: nil, except: nil, only: nil, param: nil, path: nil) {|resource| ... } ⇒ void
This method returns an undefined value.
Defines a singular resource (no index action, no :id in URL).
Useful for resources where only one instance exists, like user profile or application settings.
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 |
# File 'lib/apiwork/api/base.rb', line 509 def resource( name, concerns: nil, constraints: nil, contract: nil, controller: nil, defaults: nil, except: nil, only: nil, param: nil, path: nil, &block ) @root_resource.resource( name, concerns:, constraints:, contract:, controller:, defaults:, except:, only:, param:, path:, &block ) end |
.resources(name, concerns: nil, constraints: nil, contract: nil, controller: nil, defaults: nil, except: nil, only: nil, param: nil, path: nil) {|resource| ... } ⇒ void
This method returns an undefined value.
Defines a RESTful resource with standard CRUD actions.
This is the main method for declaring API endpoints. Creates routes for index, show, create, update, destroy actions. Nested resources and custom actions can be defined in the block.
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 |
# File 'lib/apiwork/api/base.rb', line 442 def resources( name, concerns: nil, constraints: nil, contract: nil, controller: nil, defaults: nil, except: nil, only: nil, param: nil, path: nil, &block ) @root_resource.resources( name, concerns:, constraints:, contract:, controller:, defaults:, except:, only:, param:, path:, &block ) end |
.scoped_enum_name(scope, name) ⇒ Object
793 794 795 |
# File 'lib/apiwork/api/base.rb', line 793 def scoped_enum_name(scope, name) enum_registry.scoped_name(scope, name) end |
.scoped_type_name(scope, name) ⇒ Object
789 790 791 |
# File 'lib/apiwork/api/base.rb', line 789 def scoped_type_name(scope, name) type_registry.scoped_name(scope, name) end |
.transform_key(key) ⇒ Object
725 726 727 728 729 730 731 732 733 734 735 736 737 |
# File 'lib/apiwork/api/base.rb', line 725 def transform_key(key) key_string = key.to_s return key_string if key_string.match?(/\A[A-Z]+\z/) case key_format when :camel then key_string.camelize(:lower) when :pascal then key_string.camelize when :kebab then key_string.dasherize when :underscore then key_string.underscore else key_string end end |
.transform_path(path) ⇒ Object
707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 |
# File 'lib/apiwork/api/base.rb', line 707 def transform_path(path) path_string = path.to_s return path_string if @path_format == :keep return path_string if path_string == '/' path_string.split('/').map do |segment| next segment if segment.empty? case @path_format when :camel then segment.camelize(:lower) when :pascal then segment.camelize when :kebab then segment.dasherize when :underscore then segment.underscore else segment end end.join('/') end |
.translate(*segments, default: nil) ⇒ Object
702 703 704 705 |
# File 'lib/apiwork/api/base.rb', line 702 def translate(*segments, default: nil) key = :"apiwork.apis.#{locale_key}.#{segments.join('.')}" I18n.translate(key, default:) end |
.type?(name, scope: nil) ⇒ Boolean
773 774 775 |
# File 'lib/apiwork/api/base.rb', line 773 def type?(name, scope: nil) type_registry.exists?(name, scope:) end |
.type_definition(name, scope: nil) ⇒ Object
777 778 779 |
# File 'lib/apiwork/api/base.rb', line 777 def type_definition(name, scope: nil) type_registry.find(name, scope:) end |
.union(name, deprecated: false, description: nil, discriminator: nil, example: nil) {|union| ... } ⇒ void
This method returns an undefined value.
Defines a discriminated union type.
314 315 316 |
# File 'lib/apiwork/api/base.rb', line 314 def union(name, deprecated: false, description: nil, discriminator: nil, example: nil, &block) register_union(name, deprecated:, description:, discriminator:, example:, &block) end |
.with_options(options = {}) {|resource| ... } ⇒ void
This method returns an undefined value.
Applies options to all nested resource definitions.
Useful for applying common configuration to a group of resources. Accepts the same options as #resources: only, except, defaults, constraints, controller, param, path.
596 597 598 |
# File 'lib/apiwork/api/base.rb', line 596 def ( = {}, &block) @root_resource.(, &block) end |