axn-mcp 0.1.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +209 -0
- data/README.md +459 -100
- data/lib/axn/mcp/annotations.rb +30 -0
- data/lib/axn/mcp/invocation.rb +124 -0
- data/lib/axn/mcp/serializer.rb +17 -31
- data/lib/axn/mcp/tool.rb +52 -139
- data/lib/axn/mcp/version.rb +1 -1
- data/lib/axn/mcp/wrap.rb +173 -0
- data/lib/axn/mcp.rb +101 -8
- metadata +10 -11
- data/Rakefile +0 -13
- data/lib/axn/mcp/config.rb +0 -27
- data/lib/axn/mcp/field_declarations.rb +0 -21
- data/lib/axn/mcp/schema_builder.rb +0 -224
data/lib/axn/mcp.rb
CHANGED
|
@@ -3,20 +3,113 @@
|
|
|
3
3
|
require "date"
|
|
4
4
|
require "axn"
|
|
5
5
|
require "mcp"
|
|
6
|
+
require "active_support/deprecation"
|
|
7
|
+
require "active_support/isolated_execution_state"
|
|
6
8
|
|
|
7
9
|
require_relative "mcp/version"
|
|
8
|
-
|
|
9
|
-
require_relative "mcp/serializer"
|
|
10
|
-
require_relative "mcp/schema_builder"
|
|
11
|
-
require_relative "mcp/field_declarations"
|
|
12
|
-
require_relative "mcp/tool"
|
|
10
|
+
Axn::Extensions.config.register_semantic_hint(:open_world, :closed_world)
|
|
13
11
|
|
|
14
12
|
module Axn
|
|
15
13
|
module MCP
|
|
16
|
-
|
|
14
|
+
extend Axn::Configurable
|
|
15
|
+
extend Axn::Tools::AdapterRoots
|
|
16
|
+
extend Axn::Tools::AdapterSerialization
|
|
17
|
+
|
|
18
|
+
config_namespace :mcp
|
|
19
|
+
|
|
20
|
+
# Base error for this gem, marked with core's `Axn::Error` boundary (PRO-2997) so a consumer's
|
|
21
|
+
# `rescue Axn::Error` catches it alongside axn core's errors and the other adapter gems'.
|
|
22
|
+
# `Axn::Error` is a marker module -- including it adds no ancestry. New gem errors subclass this.
|
|
23
|
+
class Error < StandardError
|
|
24
|
+
include Axn::Error
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class SchemaError < Error; end
|
|
28
|
+
|
|
29
|
+
# `extend Axn::Tools::AdapterRoots` declares a validated `tool_roots` directory list (default
|
|
30
|
+
# []); the registry reads `Axn::MCP.config.tool_roots` for directory-based tool membership.
|
|
31
|
+
# Ship the shared `agent_tools` convention as the default root, so an Axn under `app/agent_tools/`
|
|
32
|
+
# is exposed as an MCP tool out of the box (no explicit `tool :mcp` needed) -- and, since
|
|
33
|
+
# axn-ruby_llm defaults to the same dir, the same tool is exposed over ruby_llm too.
|
|
34
|
+
# `tool_roots_default` re-declares the setting through core's own `setting` path -- overriding
|
|
35
|
+
# AdapterRoots' empty default while reusing its broad-path validation (which rejects widening a
|
|
36
|
+
# root to `app/`/`.`/`actions`/a `..` traversal), and validating EAGERLY here rather than at the
|
|
37
|
+
# registry's first read -- so an app's own assignment still wins and `config.reset!(:tool_roots)`
|
|
38
|
+
# returns to this default rather than core's `[]`.
|
|
39
|
+
tool_roots_default %w[agent_tools]
|
|
40
|
+
|
|
41
|
+
setting :present_as, default: :structured, one_of: %i[structured message], overridable: true
|
|
42
|
+
|
|
43
|
+
# When true, serializing a successful result's `exposes` values rejects any value that has no
|
|
44
|
+
# JSON rendering its author declared -- one that would otherwise ship as an opaque blob like
|
|
45
|
+
# `"#<User:0x...>"` (or, in Rails, ActiveSupport's generic instance-variable dump). The rejection
|
|
46
|
+
# raises during serialization; Invocation's guard turns that into an error response (and pages
|
|
47
|
+
# `on_exception`), so the tool call fails cleanly rather than shipping the blob. Default `false`:
|
|
48
|
+
# for an LLM tool result an ugly-but-honest string usually beats a failed call, so the opaque
|
|
49
|
+
# rendering ships. Applies ONLY to outbound `exposes` serialization, not to inbound argument
|
|
50
|
+
# handling. (Values with no *honest* JSON form -- cycles, non-finite Floats, non-UTF-8-encodable
|
|
51
|
+
# bytes, colliding Hash keys -- fail regardless of this flag; it governs only the extra "was this
|
|
52
|
+
# rendering author-declared?" check.)
|
|
53
|
+
declare_reject_opaque_exposed_values! default: false
|
|
17
54
|
|
|
18
|
-
|
|
19
|
-
|
|
55
|
+
# Per-tool MCP metadata, declarable on the class via `configure(:mcp) { |c| c.title = "..." }`
|
|
56
|
+
# so it survives the zero-arg `Axn::MCP.tools` path (which calls `wrap` with no kwargs). Each
|
|
57
|
+
# defaults to nil -- left at `::MCP::Tool`'s own default unless set -- and an explicit `wrap`
|
|
58
|
+
# kwarg still wins over the configured value. Loosely typed (no validation here); `::MCP::Tool`
|
|
59
|
+
# validates the shapes when they're applied.
|
|
60
|
+
setting :title, default: nil, overridable: true
|
|
61
|
+
setting :icons, default: nil, overridable: true
|
|
62
|
+
setting :meta, default: nil, overridable: true
|
|
63
|
+
setting :annotations, default: nil, overridable: true
|
|
64
|
+
|
|
65
|
+
# Shared deprecator for this gem's own deprecated API (e.g. the legacy annotation
|
|
66
|
+
# bang-methods -- see lib/axn/mcp/tool.rb). A dedicated ActiveSupport::Deprecation instance,
|
|
67
|
+
# not the old global ActiveSupport::Deprecation.warn, so a consuming Rails app can register it
|
|
68
|
+
# (`Rails.application.deprecators[:axn_mcp] = Axn::MCP.deprecator`) and govern its behavior
|
|
69
|
+
# (silence in test, raise in CI, etc.) the same way it already does for its own deprecations.
|
|
70
|
+
def self.deprecator
|
|
71
|
+
@deprecator ||= ActiveSupport::Deprecation.new("1.0", "axn-mcp")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The live MCP server context for the current wrapped-tool call -- an `MCP::ServerContext` over a
|
|
75
|
+
# real `MCP::Server`, or whatever raw value was passed as `server_context:` to a direct call --
|
|
76
|
+
# or `nil` outside a wrapped `#call`. This is the MCP-specific handle for transport *capabilities*
|
|
77
|
+
# (`Axn::MCP.server_context.report_progress(...)`, `.cancelled?`), which are not ambient data and
|
|
78
|
+
# don't survive `ambient_context`'s declared-key filtering. Ambient *data* still belongs in
|
|
79
|
+
# `ambient_context` (`expects :user_id, on: :ambient_context`), which `wrap` spreads from the same
|
|
80
|
+
# server_context and which stays adapter-agnostic. A tool reaching for this is knowingly
|
|
81
|
+
# MCP-coupled -- appropriate, since these operations are MCP-transport-only.
|
|
82
|
+
def self.server_context
|
|
83
|
+
ActiveSupport::IsolatedExecutionState[:__axn_mcp_server_context]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Sets Axn::MCP.server_context for the duration of the block. Uses ActiveSupport's
|
|
87
|
+
# IsolatedExecutionState (thread- or fiber-scoped per the configured isolation_level), matching
|
|
88
|
+
# how axn core scopes its own per-execution state and CurrentAttributes -- so this stays correct
|
|
89
|
+
# under a Fiber scheduler rather than silently leaking across fibers a raw Thread-local would.
|
|
90
|
+
# Restores the previous value on the way out so nested wrapped calls compose.
|
|
91
|
+
def self.with_server_context(value)
|
|
92
|
+
previous = ActiveSupport::IsolatedExecutionState[:__axn_mcp_server_context]
|
|
93
|
+
ActiveSupport::IsolatedExecutionState[:__axn_mcp_server_context] = value
|
|
94
|
+
yield
|
|
95
|
+
ensure
|
|
96
|
+
ActiveSupport::IsolatedExecutionState[:__axn_mcp_server_context] = previous
|
|
20
97
|
end
|
|
98
|
+
|
|
99
|
+
# Register :mcp with axn core's process-global tool registry, passing this module as the config
|
|
100
|
+
# source (PRO-2943/PRO-2944) so the registry reads `Axn::MCP.config.tool_roots` for
|
|
101
|
+
# directory-based membership. A consumer builds its server tool list from `Axn::MCP.tools`
|
|
102
|
+
# (`Axn::Tools.for(:mcp)`) -- resolving directory-root grants, an explicit `tool`/`tool :mcp`
|
|
103
|
+
# declaration, and implicit `configure(:mcp)` membership -- instead of a hand-maintained array.
|
|
104
|
+
Axn::Tools.register_adapter(:mcp, self)
|
|
21
105
|
end
|
|
22
106
|
end
|
|
107
|
+
|
|
108
|
+
# Required after the module above extends Axn::Configurable, since Tool calls
|
|
109
|
+
# Axn::MCP.overrides at class-definition time. (Setting declaration order is
|
|
110
|
+
# free, but the extend must happen first.)
|
|
111
|
+
require_relative "mcp/annotations"
|
|
112
|
+
require_relative "mcp/serializer"
|
|
113
|
+
require_relative "mcp/invocation"
|
|
114
|
+
require_relative "mcp/tool"
|
|
115
|
+
require_relative "mcp/wrap"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: axn-mcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kali Donovan
|
|
@@ -15,7 +15,7 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.1.0.pre.alpha.
|
|
18
|
+
version: 0.1.0.pre.alpha.6
|
|
19
19
|
- - "<"
|
|
20
20
|
- !ruby/object:Gem::Version
|
|
21
21
|
version: 0.2.0
|
|
@@ -25,7 +25,7 @@ dependencies:
|
|
|
25
25
|
requirements:
|
|
26
26
|
- - ">="
|
|
27
27
|
- !ruby/object:Gem::Version
|
|
28
|
-
version: 0.1.0.pre.alpha.
|
|
28
|
+
version: 0.1.0.pre.alpha.6
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: 0.2.0
|
|
@@ -35,20 +35,20 @@ dependencies:
|
|
|
35
35
|
requirements:
|
|
36
36
|
- - ">="
|
|
37
37
|
- !ruby/object:Gem::Version
|
|
38
|
-
version:
|
|
38
|
+
version: 0.5.0
|
|
39
39
|
- - "<"
|
|
40
40
|
- !ruby/object:Gem::Version
|
|
41
|
-
version: '
|
|
41
|
+
version: '2.0'
|
|
42
42
|
type: :runtime
|
|
43
43
|
prerelease: false
|
|
44
44
|
version_requirements: !ruby/object:Gem::Requirement
|
|
45
45
|
requirements:
|
|
46
46
|
- - ">="
|
|
47
47
|
- !ruby/object:Gem::Version
|
|
48
|
-
version:
|
|
48
|
+
version: 0.5.0
|
|
49
49
|
- - "<"
|
|
50
50
|
- !ruby/object:Gem::Version
|
|
51
|
-
version: '
|
|
51
|
+
version: '2.0'
|
|
52
52
|
description: Build MCP tools using Axn's expects/exposes contract with auto-generated
|
|
53
53
|
JSON schemas and responses.
|
|
54
54
|
email:
|
|
@@ -60,15 +60,14 @@ files:
|
|
|
60
60
|
- CHANGELOG.md
|
|
61
61
|
- LICENSE
|
|
62
62
|
- README.md
|
|
63
|
-
- Rakefile
|
|
64
63
|
- lib/axn-mcp.rb
|
|
65
64
|
- lib/axn/mcp.rb
|
|
66
|
-
- lib/axn/mcp/
|
|
67
|
-
- lib/axn/mcp/
|
|
68
|
-
- lib/axn/mcp/schema_builder.rb
|
|
65
|
+
- lib/axn/mcp/annotations.rb
|
|
66
|
+
- lib/axn/mcp/invocation.rb
|
|
69
67
|
- lib/axn/mcp/serializer.rb
|
|
70
68
|
- lib/axn/mcp/tool.rb
|
|
71
69
|
- lib/axn/mcp/version.rb
|
|
70
|
+
- lib/axn/mcp/wrap.rb
|
|
72
71
|
homepage: https://github.com/teamshares/axn-mcp
|
|
73
72
|
licenses:
|
|
74
73
|
- MIT
|
data/Rakefile
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "bundler/gem_tasks"
|
|
4
|
-
require "rspec/core/rake_task"
|
|
5
|
-
require "rubocop/rake_task"
|
|
6
|
-
|
|
7
|
-
RSpec::Core::RakeTask.new(:spec)
|
|
8
|
-
|
|
9
|
-
RuboCop::RakeTask.new
|
|
10
|
-
|
|
11
|
-
task default: %i[spec rubocop]
|
|
12
|
-
|
|
13
|
-
Rake::Task["build"].enhance([:default])
|
data/lib/axn/mcp/config.rb
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Axn
|
|
4
|
-
module MCP
|
|
5
|
-
class Config
|
|
6
|
-
VALID_MCP_TEXT_CONTENT = %i[structured message].freeze
|
|
7
|
-
|
|
8
|
-
attr_reader :mcp_text_content
|
|
9
|
-
|
|
10
|
-
def initialize
|
|
11
|
-
@mcp_text_content = :structured
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def mcp_text_content=(value)
|
|
15
|
-
self.class.validate_mcp_text_content!(value)
|
|
16
|
-
@mcp_text_content = value
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def self.validate_mcp_text_content!(value)
|
|
20
|
-
return if VALID_MCP_TEXT_CONTENT.include?(value)
|
|
21
|
-
|
|
22
|
-
raise ArgumentError,
|
|
23
|
-
"mcp_text_content must be one of #{VALID_MCP_TEXT_CONTENT.map(&:inspect).join(", ")}; got #{value.inspect}"
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Axn
|
|
4
|
-
module MCP
|
|
5
|
-
module FieldDeclarations
|
|
6
|
-
module_function
|
|
7
|
-
|
|
8
|
-
def hydrate(declarations)
|
|
9
|
-
return declarations if declarations.is_a?(Hash)
|
|
10
|
-
|
|
11
|
-
Array(declarations).each_with_object({}) do |item, acc|
|
|
12
|
-
if item.is_a?(Hash)
|
|
13
|
-
item.each { |k, v| acc[k] = v }
|
|
14
|
-
else
|
|
15
|
-
acc[item] = {}
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "date"
|
|
4
|
-
|
|
5
|
-
module Axn
|
|
6
|
-
module MCP
|
|
7
|
-
module SchemaBuilder
|
|
8
|
-
TYPE_MAP = {
|
|
9
|
-
String => "string",
|
|
10
|
-
Integer => "integer",
|
|
11
|
-
Float => "number",
|
|
12
|
-
Numeric => "number",
|
|
13
|
-
Hash => "object",
|
|
14
|
-
Array => "array",
|
|
15
|
-
TrueClass => "boolean",
|
|
16
|
-
FalseClass => "boolean",
|
|
17
|
-
Date => "string",
|
|
18
|
-
DateTime => "string",
|
|
19
|
-
Time => "string",
|
|
20
|
-
}.freeze
|
|
21
|
-
|
|
22
|
-
FORMAT_MAP = {
|
|
23
|
-
Date => "date",
|
|
24
|
-
DateTime => "date-time",
|
|
25
|
-
Time => "date-time",
|
|
26
|
-
}.freeze
|
|
27
|
-
|
|
28
|
-
EXCLUDED_FROM_SCHEMA = %i[server_context].freeze
|
|
29
|
-
|
|
30
|
-
module_function
|
|
31
|
-
|
|
32
|
-
def build_input(field_configs, subfield_configs = [])
|
|
33
|
-
properties = {}
|
|
34
|
-
required = []
|
|
35
|
-
|
|
36
|
-
subfields_by_parent = subfield_configs.group_by(&:on)
|
|
37
|
-
|
|
38
|
-
field_configs.each do |config|
|
|
39
|
-
next if EXCLUDED_FROM_SCHEMA.include?(config.field)
|
|
40
|
-
|
|
41
|
-
if config.validations[:model]
|
|
42
|
-
build_model_property(config, properties, required)
|
|
43
|
-
else
|
|
44
|
-
prop = build_property(config)
|
|
45
|
-
nested_subfields = subfields_by_parent[config.field]
|
|
46
|
-
if nested_subfields&.any? && prop[:type] == "object"
|
|
47
|
-
prop[:properties] ||= {}
|
|
48
|
-
prop[:required] ||= []
|
|
49
|
-
nested_subfields.each do |subconfig|
|
|
50
|
-
subprop = build_property(subconfig)
|
|
51
|
-
prop[:properties][subconfig.field] = subprop
|
|
52
|
-
prop[:required] << subconfig.field.to_s unless optional?(subconfig)
|
|
53
|
-
end
|
|
54
|
-
prop[:required] = nil if prop[:required].empty?
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
properties[config.field] = prop.compact
|
|
58
|
-
required << config.field.to_s unless optional?(config)
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
schema = { type: "object", properties: }
|
|
63
|
-
schema[:required] = required unless required.empty?
|
|
64
|
-
schema
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def build_output(field_configs)
|
|
68
|
-
properties = {}
|
|
69
|
-
required = []
|
|
70
|
-
|
|
71
|
-
field_configs.each do |config|
|
|
72
|
-
prop = build_property(config, for_output: true)
|
|
73
|
-
properties[config.field] = prop.compact
|
|
74
|
-
required << config.field.to_s unless optional?(config)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
schema = { type: "object", properties: }
|
|
78
|
-
schema[:required] = required unless required.empty?
|
|
79
|
-
schema
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def build_property(config, for_output: false)
|
|
83
|
-
prop = {}
|
|
84
|
-
prop[:description] = config.description if config.description
|
|
85
|
-
|
|
86
|
-
type_info = json_type_for(config.validations, for_output:)
|
|
87
|
-
prop[:type] = type_info[:type] if type_info[:type]
|
|
88
|
-
prop[:format] = type_info[:format] if type_info[:format]
|
|
89
|
-
|
|
90
|
-
prop[:default] = config.default if config.respond_to?(:default) && !config.default.nil?
|
|
91
|
-
|
|
92
|
-
if (inclusion = config.validations[:inclusion])
|
|
93
|
-
enum_values = inclusion[:in] || inclusion[:within] if inclusion.is_a?(Hash)
|
|
94
|
-
prop[:enum] = enum_values if enum_values
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
apply_structured_schema!(prop, config, for_output:)
|
|
98
|
-
|
|
99
|
-
prop
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# Combine of: (bare element baseline) and shape: (typed member contracts) into
|
|
103
|
-
# items:/properties: schema. Precedence: shape: enriches/overrides of: baseline.
|
|
104
|
-
def apply_structured_schema!(prop, config, for_output:)
|
|
105
|
-
of = config.validations[:of]
|
|
106
|
-
shape = config.validations[:shape]
|
|
107
|
-
return unless of || shape
|
|
108
|
-
|
|
109
|
-
if prop[:type] == "array"
|
|
110
|
-
items = of ? items_schema_for(of, for_output:) : {}
|
|
111
|
-
if shape
|
|
112
|
-
member_props, required = member_properties(shape[:members], for_output:)
|
|
113
|
-
base_props = items[:properties] || {}
|
|
114
|
-
items = items.merge(type: "object", properties: base_props.merge(member_props))
|
|
115
|
-
items[:required] = required unless required.empty?
|
|
116
|
-
end
|
|
117
|
-
prop[:items] = items unless items.empty?
|
|
118
|
-
elsif shape
|
|
119
|
-
# Hash / class field — shape: members are the object's own properties.
|
|
120
|
-
# If the field type is a Data.define subclass, use its members as the bare
|
|
121
|
-
# baseline so unannotated members still appear (same enrich logic as of:).
|
|
122
|
-
member_props, required = member_properties(shape[:members], for_output:)
|
|
123
|
-
type_klass = config.validations.dig(:type, :klass)
|
|
124
|
-
base_props = type_klass.is_a?(Class) && type_klass < Data ? type_klass.members.to_h { |m| [m, {}] } : {}
|
|
125
|
-
prop[:properties] = base_props.merge(member_props)
|
|
126
|
-
prop[:required] = required unless required.empty?
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Build a JSON Schema items: value from the of: validation hash.
|
|
131
|
-
def items_schema_for(of_validations, for_output: false)
|
|
132
|
-
klasses = Array(of_validations[:klass])
|
|
133
|
-
if klasses.size == 1
|
|
134
|
-
single_items_schema(klasses.first, for_output:)
|
|
135
|
-
else
|
|
136
|
-
{ anyOf: klasses.map { |k| single_items_schema(k, for_output:) } }
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def single_items_schema(klass, for_output: false)
|
|
141
|
-
if klass.is_a?(Class) && klass < Data
|
|
142
|
-
# Data.define subclass → object with named (but untyped) properties as baseline
|
|
143
|
-
{ type: "object", properties: klass.members.to_h { |m| [m, {}] } }
|
|
144
|
-
else
|
|
145
|
-
json_type_for({ type: klass }, for_output:)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Build properties/required from a shape: block's members. Recurses for nested shape/of.
|
|
150
|
-
def member_properties(members, for_output:)
|
|
151
|
-
props = {}
|
|
152
|
-
required = []
|
|
153
|
-
members.each do |m|
|
|
154
|
-
props[m.field] = build_property(m, for_output:).compact
|
|
155
|
-
required << m.field.to_s unless optional?(m)
|
|
156
|
-
end
|
|
157
|
-
[props, required]
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def build_model_property(config, properties, required)
|
|
161
|
-
model_opts = config.validations[:model]
|
|
162
|
-
klass = model_opts[:klass]
|
|
163
|
-
klass_name = klass.is_a?(Class) ? klass.name : klass.to_s
|
|
164
|
-
|
|
165
|
-
id_field = :"#{config.field}_id"
|
|
166
|
-
prop = {
|
|
167
|
-
type: "integer",
|
|
168
|
-
description: config.description || "ID of the #{klass_name} record",
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
properties[id_field] = prop.compact
|
|
172
|
-
required << id_field.to_s unless optional?(config)
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def json_type_for(validations, for_output: false)
|
|
176
|
-
if validations[:type]
|
|
177
|
-
type_opt = validations[:type]
|
|
178
|
-
klass = type_opt.is_a?(Hash) ? type_opt[:klass] : type_opt
|
|
179
|
-
klasses = Array(klass)
|
|
180
|
-
|
|
181
|
-
klass = klasses.first
|
|
182
|
-
return { type: "boolean" } if klass == :boolean
|
|
183
|
-
return { type: "string", format: "uuid" } if klass == :uuid
|
|
184
|
-
|
|
185
|
-
if TYPE_MAP.key?(klass)
|
|
186
|
-
result = { type: TYPE_MAP[klass] }
|
|
187
|
-
result[:format] = FORMAT_MAP[klass] if FORMAT_MAP.key?(klass)
|
|
188
|
-
return result
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
return { type: "object" } if for_output
|
|
192
|
-
|
|
193
|
-
return { type: "string" }
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
if validations[:inclusion]
|
|
197
|
-
inclusion = validations[:inclusion]
|
|
198
|
-
enum_values = inclusion[:in] || inclusion[:within] if inclusion.is_a?(Hash)
|
|
199
|
-
if enum_values&.any?
|
|
200
|
-
sample = enum_values.first
|
|
201
|
-
return { type: "string" } if sample.is_a?(String)
|
|
202
|
-
return { type: "integer" } if sample.is_a?(Integer)
|
|
203
|
-
return { type: "number" } if sample.is_a?(Float)
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
if validations[:numericality]
|
|
208
|
-
numericality = validations[:numericality]
|
|
209
|
-
return { type: "integer" } if numericality.is_a?(Hash) && numericality[:only_integer]
|
|
210
|
-
|
|
211
|
-
return { type: "number" }
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
return { type: "string" } if validations[:presence] || validations[:length]
|
|
215
|
-
|
|
216
|
-
{}
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def optional?(config)
|
|
220
|
-
Axn::Internal::FieldConfig.optional?(config)
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
end
|