require 'spec_helper' describe Parametric::Schema do before do Parametric.policy :flexible_bool do coerce do |v, k, c| case v when '1', 'true', 'TRUE', true true else false end end end end subject do described_class.new do field(:title).policy(:string).present field(:price).policy(:integer).meta(label: "A price") field(:status).policy(:string).options(['visible', 'hidden']) field(:tags).policy(:split).policy(:array) field(:description).policy(:string) field(:variants).policy(:array).schema do field(:name).policy(:string).present field(:sku) field(:stock).policy(:integer).default(1) field(:available_if_no_stock).policy(:boolean).policy(:flexible_bool).default(false) end end end describe "#structure" do it "represents data structure and meta data" do sc = subject.structure expect(sc[:title][:present]).to be true expect(sc[:title][:type]).to eq :string expect(sc[:price][:type]).to eq :integer expect(sc[:price][:label]).to eq "A price" expect(sc[:variants][:type]).to eq :array sc[:variants][:structure].tap do |sc| expect(sc[:name][:type]).to eq :string expect(sc[:name][:present]).to be true expect(sc[:stock][:default]).to eq 1 end end end def resolve(schema, payload, &block) yield schema.resolve(payload) end def test_schema(schema, payload, result) resolve(schema, payload) do |results| expect(results.output).to eq result end end it 'works' do test_schema(subject, { title: 'iPhone 6 Plus', price: '100.0', status: 'visible', tags: 'tag1, tag2', description: 'A description', variants: [{name: 'v1', sku: 'ABC', stock: '10', available_if_no_stock: true}] }, { title: 'iPhone 6 Plus', price: 100, status: 'visible', tags: ['tag1', 'tag2'], description: 'A description', variants: [{name: 'v1', sku: 'ABC', stock: 10, available_if_no_stock: true}] }) test_schema(subject, { title: 'iPhone 6 Plus', variants: [{name: 'v1', available_if_no_stock: '1'}] }, { title: 'iPhone 6 Plus', variants: [{name: 'v1', stock: 1, available_if_no_stock: true}] }) resolve(subject, {}) do |results| expect(results.valid?).to be false expect(results.errors['$.title']).not_to be_nil expect(results.errors['$.variants']).to be_nil expect(results.errors['$.status']).to be_nil end resolve(subject, {title: 'Foobar', variants: [{name: 'v1'}, {sku: '345'}]}) do |results| expect(results.valid?).to be false expect(results.errors['$.variants[1].name']).not_to be_nil end end it "ignores nil fields if using :declared policy" do schema = described_class.new do field(:id).type(:integer) field(:title).declared.type(:string) end resolve(schema, {id: 123}) do |results| expect(results.output.keys).to eq [:id] end end describe "#policy" do it "applies policy to all fields" do subject.policy(:declared) resolve(subject, {}) do |results| expect(results.valid?).to be true expect(results.errors.keys).to be_empty end end it "replaces previous policies" do subject.policy(:declared) subject.policy(:present) resolve(subject, {title: "hello"}) do |results| expect(results.valid?).to be false expect(results.errors.keys).to match_array(%w( $.price $.status $.tags $.description $.variants )) end end it "applies :noop policy to all fields" do subject.policy(:noop) resolve(subject, {}) do |results| expect(results.valid?).to be false expect(results.errors['$.title']).not_to be_nil end end end describe "#merge" do context "no options" do let!(:schema1) { described_class.new do field(:title).policy(:string).present field(:price).policy(:integer) end } let!(:schema2) { described_class.new do field(:price).policy(:string) field(:description).policy(:string) end } it "returns a new schema adding new fields and updating existing ones" do new_schema = schema1.merge(schema2) expect(new_schema.fields.keys).to match_array([:title, :price, :description]) # did not mutate original expect(schema1.fields[:price].meta_data[:type]).to eq :integer expect(new_schema.fields[:title].meta_data[:type]).to eq :string expect(new_schema.fields[:price].meta_data[:type]).to eq :string end it 'can merge from a block' do new_schema = schema1.merge do field(:price).policy(:string) field(:description).policy(:string) end expect(schema1.fields[:price].meta_data[:type]).to eq :integer expect(new_schema.fields[:title].meta_data[:type]).to eq :string expect(new_schema.fields[:price].meta_data[:type]).to eq :string end end context "with options" do let!(:schema1) { described_class.new(price_type: :integer, label: "Foo") do |opts| field(:title).policy(:string).present field(:price).policy(opts[:price_type]).meta(label: opts[:label]) end } let!(:schema2) { described_class.new(price_type: :string) do field(:description).policy(:string) end } it "inherits options" do new_schema = schema1.merge(schema2) expect(new_schema.fields[:price].meta_data[:type]).to eq :string expect(new_schema.fields[:price].meta_data[:label]).to eq "Foo" end it "re-applies blocks with new options" do new_schema = schema1.merge(schema2) expect(new_schema.fields.keys).to match_array([:title, :price, :description]) # did not mutate original expect(schema1.fields[:price].meta_data[:type]).to eq :integer expect(new_schema.fields[:title].meta_data[:type]).to eq :string expect(new_schema.fields[:price].meta_data[:type]).to eq :string end end end describe "#clone" do let!(:schema1) { described_class.new do |opts| field(:id).present field(:title).policy(:string).present field(:price) end } it "returns a copy that can be further manipulated" do schema2 = schema1.clone.policy(:declared).ignore(:id) expect(schema1.fields.keys).to match_array([:id, :title, :price]) expect(schema2.fields.keys).to match_array([:title, :price]) results1 = schema1.resolve(id: "abc", price: 100) expect(results1.errors.keys).to eq ["$.title"] results2 = schema2.resolve(id: "abc", price: 100) expect(results2.errors.keys).to eq [] end end context 'yielding schema to definition, to preserve outer context' do it 'yields schema instance and options to definition block, can access outer context' do schema1 = described_class.new do field(:name).type(:string) end schema2 = described_class.new do |sc, _opts| sc.field(:user).schema schema1 end out = schema2.resolve(user: { name: 'Joe' }).output expect(out[:user][:name]).to eq 'Joe' end end describe '#tagged_one_of for multiple sub-schemas' do let(:user_schema) do described_class.new do field(:name).type(:string).present field(:age).type(:integer).present end end let(:company_schema) do described_class.new do field(:name).type(:string).present field(:company_code).type(:string).present end end it 'picks the right sub-schema' do schema = described_class.new do |sc, _| sc.field(:type).type(:string) sc.field(:sub).type(:object).tagged_one_of do |sub| sub.index_by(:type) sub.on('user', user_schema) sub.on('company', company_schema) end end result = schema.resolve(type: 'user', sub: { name: 'Joe', age: 30 }) expect(result.valid?).to be true expect(result.output).to eq({ type: 'user', sub: { name: 'Joe', age: 30 } }) result = schema.resolve(type: 'company', sub: { name: 'ACME', company_code: 123 }) expect(result.valid?).to be true expect(result.output).to eq({ type: 'company', sub: { name: 'ACME', company_code: '123' } }) result = schema.resolve(type: 'company', sub: { name: nil, company_code: 123 }) expect(result.valid?).to be false expect(result.errors['$.sub.name']).not_to be_empty result = schema.resolve(type: 'foo', sub: { name: 'ACME', company_code: 123 }) expect(result.valid?).to be false end it 'can be assigned to instance and reused' do user_or_company = Parametric::TaggedOneOf.new do |sub| sub.on('user', user_schema) sub.on('company', company_schema) end schema = described_class.new do |sc, _| sc.field(:type).type(:string) sc.field(:sub).type(:object).tagged_one_of(user_or_company.index_by(:type)) end result = schema.resolve(type: 'user', sub: { name: 'Joe', age: 30 }) expect(result.valid?).to be true expect(result.output).to eq({ type: 'user', sub: { name: 'Joe', age: 30 } }) end specify '#structure' do user_or_company = Parametric::TaggedOneOf.new do |sub| sub.on('user', user_schema) sub.on('company', company_schema) end schema = described_class.new do |sc, _| sc.field(:type).type(:string) sc.field(:sub).type(:object).tagged_one_of(user_or_company.index_by(:type)) end structure = schema.structure structure.dig(:sub).tap do |sub| expect(sub[:type]).to eq :object expect(sub[:one_of][0][:name][:type]).to eq :string expect(sub[:one_of][0][:name][:required]).to be true expect(sub[:one_of][0][:name][:present]).to be true expect(sub[:one_of][0][:age][:type]).to eq :integer expect(sub[:one_of][1][:company_code][:type]).to eq :string end end end describe "#ignore" do it "ignores fields" do s1 = described_class.new.ignore(:title, :status) do field(:status) field(:title).policy(:string).present field(:price).policy(:integer) end output = s1.resolve(status: "draft", title: "foo", price: "100").output expect(output).to eq({price: 100}) end it "ignores when merging" do s1 = described_class.new do field(:status) field(:title).policy(:string).present end s1 = described_class.new.ignore(:title, :status) do field(:price).policy(:integer) end output = s1.resolve(title: "foo", status: "draft", price: "100").output expect(output).to eq({price: 100}) end it "returns self so it can be chained" do s1 = described_class.new do field(:status) field(:title).policy(:string).present end expect(s1.ignore(:status)).to eq s1 end end end