Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RailsのValidatesをSwift Macrosで再現してみた

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

RailsのValidatesをSwift Macrosで再現してみた

Avatar for Takuma Shimizu

Takuma Shimizu

March 06, 2026
Tweet

More Decks by Takuma Shimizu

Other Decks in Programming

Transcript

  1. "DUJWF3FDPSE 3VCZPO3BJMT ͷόϦσʔγϣϯ class Article < ApplicationRecord validates :title, :body,

    presence: true validates :status, inclusion: { in: %w[draft published] } validates :title, length: { maximum: 100 } validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } end > article.valid? #=> true or false
  2. 4XJGU.BDSPTͰ࠶ݱ @Validatable struct Article { var title: String var body:

    String var status: Status var email: String? #Validation(\Self.title, \.body, presence: .required) #Validation(\Self.status, inclusion: [.draft, .published]) #Validation(\Self.title, countWithin: ...100) #Validation(\Self.email, format: /^[a-zA-Z0-9._%+-]+@.+\..{2,}$/) } > article.isValid > try article.validate() IUUQTHJUIVCDPNIPLVSPOTXJGUWBMJEBUJPOT IPLVSPOTXJGUWBMJEBUJPOT
  3. "DUJWF3FDPSE 4XJGU7BMJEBUJPOT validates :title, :body, presence: true #Validation(\Self.title, \.body, presence:

    .required) validates :status, inclusion: { in: %w[…] } #Validation(\Self.status, inclusion: […]) validates :title, length: { maximum: 100 } #Validation(\Self.title, countWithin: ...100) validates :email, format: { with: /…/} #Validation(\Self.email, format: /…/) ΠϯλϑΣʔε͸֓Ͷಉ͡ʹͭͭ͠ɺࡉ͔͍෦෼͸4XJGU΍"QQMFϑϨʔϜϫʔΫͷޠኮΛ࢖༻ɻ
  4. ܕ҆શੑ "DUJWF3FDPSE validates :age, length: { minimum: 16 } //

    ⚠ ܻ਺νΣοΫͱͳΓҙਤ͠ͳ͍ݕূ݁ՌʹͳΔɻ͜ͷ৔߹numericalityݕূΛ͢Δͷ ͕ਖ਼͍͠ɻ 4XJGU7BMJEBUJPOT #Validation(\Self.age, countWithin: 16…) // ❌ Macro 'Validation(_:countWithin:where:)' requires that 'Int' conform to 'Collection'
  5. 4XJGUͳΒͰ͸ͷ੍໿ w ݕূର৅Λ,FZ1BUIͰࢦఆ͢Δඞཁ͕͋Δ w 'SFFTUBOEJOHNBDSPͳͷͰɺपลͷίϯςΩετΛ֬ೝͰ͖ͳ͍ w Φʔόʔϩʔυ͕૿͕͑ͪ w ੩తܕ෇͚ͳͷͰɺݕূର৅ͱݕূ஋ͷܕͷؔ܎ੑ͝ͱʹఆ͕ٛඞཁ w

    ઌͷ੍໿ʢ,FZ1BUIࢦఆʣͷͨΊɺ0QUJPOBM൛ͱඇ0QUJPOBM൛ͷ྆ํͷఆ͕ٛඞཁ w ͜ͷ੍໿ʹΑΓɺఆٛ਺͕୯७ʹഒʹͳΔ w IUUQTHJUIVCDPNIPLVSPOTXJGUWBMJEBUJPOT4PVSDFT7BMJEBUJPOT.BDSPTTXJGU ޙऀ͸࣮૷ଆͷ౎߹ͳͷͰɺར༻ऀʹӨڹ͕͋Δͷ͸લऀͱͳΔɻ
  6. .BDSPͷ࣮૷ w #Validationࣗମ͸Կ΋ੜ੒͠ͳ͍ struct ValidationMacro: DeclarationMacro { public static func

    expansion(...) throws -> [DeclSyntax] { return [] // ۭ഑ྻΛฦ͠ίʔυੜ੒΋ͳ͍ } } w @ValidatableϚΫϩଆϝϯόʔ಺ͷ#ValidationͷଘࡏΛ֬ೝ͠ίʔυੜ੒ Λߦ͏ w IUUQTHJUIVCDPNIPLVSPOTXJGUWBMJEBUJPOT4PVSDFT7BMJEBUJPOT.BDSPT7BMJEBUBCMF.BDSPTXJGU
  7. @Validatable struct User { var name: String var age: UInt

    var email: String? var bio: String? var password: String #Validation(\Self.name, presence: .required) #Validation(\Self.age, comparison: .greaterThan(16)) #Validation(\Self.email, format: /…/, presence: .required(allowsNil: true)) #Validation(\Self.bio, presence: .none) #Validation(\Self.password, presence: .required(allowsEmpty: true)) }
  8. @Validatable struct User { var name: String var age: UInt

    var email: String? var bio: String? var password: String #Validation(\Self.name, presence: .required) #Validation(\Self.age, comparison: .greaterThan(16)) #Validation(\Self.email, format: /…/, presence: .required(allowsNil: true)) #Validation(\Self.bio, presence: .none) #Validation(\Self.password, presence: .required(allowsEmpty: true)) } ˣੜ੒ίʔυ SFTVMUCVJMEFS  extension User: Validations.Validatable { var validation: some Validator { Presence(of: self[keyPath: \Self.name]).presence(.required) .errorKey(Self.self, \Self.name) Comparison(of: self[keyPath: \Self.age], .greaterThan(16)) .errorKey(Self.self, \Self.age) Format(of: self[keyPath: \Self.email], with: /…/).presence(.required(allowsNil: true)) .errorKey(Self.self, \Self.email) Presence(of: self[keyPath: \Self.bio]).presence(.none) .errorKey(Self.self, \Self.bio) Presence(of: self[keyPath: \Self.password]).presence(.required(allowsEmpty: true)) .errorKey(Self.self, \Self.password) } }