루비는 Psych로 YAML을 읽어 해시와 배열로 돌려주는데, 매핑마다 작은 클래스를 두면 data['db']['host']가 이어지는 코드보다 설정이나 픽스처를 쓰는 코드가 훨씬 읽기 좋아집니다. 이 변환기는 자바스크립트 yaml 라이브러리(YAML 1.2 코어 스키마)로 YAML을 읽고 앵커와 병합 키를 펼친 뒤, 같은 위치 값의 모양을 합쳐서 매핑마다 클래스 하나를 씁니다. attr_reader, 키워드 인자 initialize, from_hash, from_json, to_h, to_json이 들어갑니다.
루비에는 필드 타입이 없어서 클래스에도 타입이 없습니다. YAML이 정하는 것은 어떤 키가 있는지, 어떤 클래스가 중첩되는지, 어떤 키가 선택인지입니다. 매핑 목록은 모든 항목의 속성을 합친 항목 클래스 하나가 되고, 일부 항목에 없는 키는 nil 기본값이 있는 키워드 인자가 됩니다. 옵션은 루트 클래스 이름 하나뿐입니다(기본 Payload, 파일 이름은 payload.rb).
예제 세 가지(API 응답, 앵커와 병합 키, 엣지 케이스)를 한 번에 불러올 수 있고, 입력하는 동안 Ruby 코드가 갱신됩니다. 작업이 브라우저 탭 안에서만 이루어지므로 업로드되는 것이 없고, 입력 한도는 20MiB입니다.
사용 방법
YAML 붙여 넣기 또는 예제 고르기
YAML을 붙여 넣거나
.yaml파일을 편집기에 끌어다 놓고, 아니면 예제 칩에서 골라 시작하세요. 매핑과 목록은 루트로 쓸 수 있고, 스칼라 하나만 있으면 클래스를 만들 수 없어 오류가 표시됩니다.루트 클래스 이름 정하기
루트 타입 이름 칸에 이름을 씁니다. PascalCase로 바뀌어 중첩 클래스의 접두어가 되고(
PayloadItem), 내려받는 파일 이름은payload.rb처럼 스네이크 표기가 됩니다.문서와 병합 키 고르기
문서가 여러 개일 때 자동은 목록을 만들고, 항상 배열은 하나여도 그렇게 하며, 첫 문서만은 첫 문서만 남깁니다. 병합 키(<<) 펼치기는 물려받은 키가 클래스의 속성이 될지 정합니다.
생성된 클래스 확인
루트 클래스가 먼저 나오고 중첩 클래스가 만난 순서대로 이어집니다. 어떤 키워드 인자가 필수이고 어떤 것이
nil기본값인지 확인하세요.복사 또는 저장
코드를 복사하거나
.rb파일로 내려받습니다. 맨 위에require "json"줄이 있고 클래스가 연달아 쓰이므로, 프로젝트가 파일당 클래스 하나를 불러온다면 나눠서 쓰세요.
작동 원리
생성되는 클래스의 구성
매핑마다 키별 attr_reader, 키워드 인자를 받아 인스턴스 변수에 담는 initialize, 문자열 키를 읽는 self.from_hash(data), 먼저 JSON.parse를 호출하는 self.from_json(json), 되돌리는 to_h와 to_json이 있는 클래스가 나옵니다. 중첩 매핑은 중첩 클래스의 from_hash로, 매핑 목록은 map { |v| ... }로 만듭니다.
필수 키워드 인자는 선택 인자 뒤에 올 수 없으므로 nil이 기본값인 속성은 initialize의 맨 끝으로 옮겨지고, attr_reader, from_hash, to_h는 YAML 순서를 유지합니다. 값 자체는 그대로 전달되어 Psych가 읽은 숫자가 그대로 남습니다.
| YAML | Ruby 클래스 | 비고 |
|---|---|---|
| name: api | attr_reader :name | 스칼라 타입은 무엇이든 |
| max-conn: 5 | max_conn, data['max-conn'] | 이름은 스네이크 표기, 키는 그대로 |
| class: x | class_value | 예약어와 메서드 이름에는 _value |
| limits: {cpu: 0.5} | PayloadLimits.from_hash(...) | 중첩 클래스 |
| items: [{a: 1}, {a: 2}] | map { |v| PayloadItem.from_hash(v) } | 항목 클래스 하나를 공유 |
| names: [x, y] | data['names'] | 스칼라 목록은 그대로 전달 |
| 일부 항목에 없는 키 | b: nil | 선택 키워드 인자 |
| 일부 항목에서 note: ~ | note: nil | 이것도 선택 |
이름과 충돌
키는 스네이크 표기 속성 이름이 되어 max-conn은 max_conn, Foo Bar는 foo_bar입니다. class 같은 루비 키워드나 클래스가 직접 정의하는 메서드(initialize, from_hash, from_json, to_h, to_json)를 덮어쓰게 될 이름에는 _value가 붙습니다. Hash나 String 같은 코어 클래스를 가리게 될 클래스 이름에는 Data가 붙습니다.
두 키가 같은 속성 이름이 되면 나중 것에 숫자 접미사가 붙어 둘 다 살아남고, 읽기와 쓰기에는 항상 원래 키를 씁니다. ASCII 글자가 없는 키는 field로 이름이 붙습니다.
YAML이라서 생기는 점
별칭은 복사본으로 펼쳐지고 << 병합 키도 기본으로 펼쳐지므로 앵커에서 물려받는 매핑은 물려받은 속성을 모두 갖습니다. 스위치를 끄면 <<에서 만든 속성이 중첩 클래스와 함께 나타납니다. 여러 문서는 목록으로 읽혀, 루트가 항목 클래스의 배열이라는 주석이 붙고 항목 클래스만 생성됩니다.
루비의 Psych는 YAML 1.1 방식으로 값을 읽습니다. yes는 true, 0123은 8진수 83, 날짜는 Date 객체가 됩니다. 생성된 클래스는 아무것도 변환하지 않고 로더가 돌려준 값을 그대로 담습니다. 파일에 필요한 옵션을 준 YAML.safe_load를 쓰거나 값을 따옴표로 감싸세요.
실제 예시
name: api
replicas: 3
enabled: yesrequire "json"
class Payload
attr_reader :name, :replicas, :enabled
def initialize(name:, replicas:, enabled:)
@name = name
@replicas = replicas
@enabled = enabled
end
def self.from_hash(data)
new(
name: data['name'],
replicas: data['replicas'],
enabled: data['enabled']
)
end
def self.from_json(json)
from_hash(JSON.parse(json))
end
def to_h
{
'name' => @name,
'replicas' => @replicas,
'enabled' => @enabled
}
end
def to_json(*args)
to_h.to_json(*args)
end
end아무것도 변환하지 않고 로더가 준 값을 그대로 저장합니다. 그래서 enabled에는 Psych가 yes를 읽은 방식(YAML 1.1의 true)이 그대로 들어갑니다.
items:
- {a: 1}
- {a: 2, b: x}
names: [x, y]require "json"
class Payload
attr_reader :items, :names
def initialize(items:, names:)
@items = items
@names = names
end
def self.from_hash(data)
new(
items: data['items'].map { |v| PayloadItem.from_hash(v) },
names: data['names']
)
end
def self.from_json(json)
from_hash(JSON.parse(json))
end
def to_h
{
'items' => @items.map { |v| v.to_h },
'names' => @names
}
end
def to_json(*args)
to_h.to_json(*args)
end
end
class PayloadItem
attr_reader :a, :b
def initialize(a:, b: nil)
@a = a
@b = b
end
def self.from_hash(data)
new(
a: data['a'],
b: data['b']
)
end
def self.from_json(json)
from_hash(JSON.parse(json))
end
def to_h
{
'a' => @a,
'b' => @b
}
end
def to_json(*args)
to_h.to_json(*args)
end
end두 항목이 PayloadItem 하나를 공유합니다. b는 첫 항목에 없어서 nil이 기본값인 선택 키워드 인자가 되고, 객체 목록은 map으로 다시 만듭니다.
note: ~
name: xrequire "json"
class Payload
attr_reader :note, :name
def initialize(name:, note: nil)
@note = note
@name = name
end
def self.from_hash(data)
new(
note: data['note'],
name: data['name']
)
end
def self.from_json(json)
from_hash(JSON.parse(json))
end
def to_h
{
'note' => @note,
'name' => @name
}
end
def to_json(*args)
to_h.to_json(*args)
end
endnote는 YAML에서 null이라 기본값이 nil이고 initialize에서 필수인 name 뒤에 옵니다. 그 밖의 곳은 YAML 순서를 유지합니다.
initialize: 1
to_h: 2
Foo Bar: 3require "json"
class Payload
attr_reader :initialize_value, :to_h_value, :foo_bar
def initialize(initialize_value:, to_h_value:, foo_bar:)
@initialize_value = initialize_value
@to_h_value = to_h_value
@foo_bar = foo_bar
end
def self.from_hash(data)
new(
initialize_value: data['initialize'],
to_h_value: data['to_h'],
foo_bar: data['Foo Bar']
)
end
def self.from_json(json)
from_hash(JSON.parse(json))
end
def to_h
{
'initialize' => @initialize_value,
'to_h' => @to_h_value,
'Foo Bar' => @foo_bar
}
end
def to_json(*args)
to_h.to_json(*args)
end
endinitialize와 to_h는 클래스가 직접 정의하는 메서드를 덮어쓰게 되므로 _value가 붙고, 읽기와 쓰기에는 원래 키를 그대로 씁니다.
id: 1
name: a
---
id: 2require "json"
# The root JSON value is an array of PayloadItem
class PayloadItem
attr_reader :id, :name
def initialize(id:, name: nil)
@id = id
@name = name
end
def self.from_hash(data)
new(
id: data['id'],
name: data['name']
)
end
def self.from_json(json)
from_hash(JSON.parse(json))
end
def to_h
{
'id' => @id,
'name' => @name
}
end
def to_json(*args)
to_h.to_json(*args)
end
end자동 모드는 문서 둘을 목록으로 만듭니다. 루트가 배열이라는 주석과 함께 항목 클래스만 생성되고, 두 번째 문서에 없는 name은 선택입니다.
자주 하는 실수
Psych가 파일을 다르게 읽을 수 있음
루비의 Psych는
yes를 true,0123을 8진수, 날짜를Date로 읽고,YAML.safe_load는 허용하지 않으면 별칭과 날짜를 거부합니다. 클래스는 로더가 돌려준 값을 그대로 담으므로 텍스트로 남아야 하는 값은 따옴표로 감싸세요.선택 여부는 여러 매핑을 보고 추정
같은 위치의 일부 매핑에 키가 없거나 null일 때만 속성의 기본값이
nil이 됩니다. 샘플 레코드가 하나면 모든 키워드가 필수이므로 여러 개를 붙여 넣으세요.타입 검사가 없음
클래스는 값을 검증하거나 변환하지 않습니다. 샘플에서 숫자였던 자리에 문자열이 와도 그대로 받으므로, 필요하면 검사를 넣거나 스키마 젬을 쓰세요.
루트가 목록이면 루트 클래스가 없음
목록이면 항목 클래스만 쓰이고 주석이 루트가 배열이라고 알려 줍니다.
YAML.load_file(path).map { |h| PayloadItem.from_hash(h) }처럼 직접 이어 주세요.모두 한 파일에 들어 있음
클래스가 연달아 쓰입니다. 오토로더(예: Zeitwerk)가 파일당 상수 하나를 기대하면 따로 파일로 나누세요.
심볼 키는 쓰지 않음
from_hash는JSON.parse와YAML.load가 돌려주는 문자열 키를 읽습니다.symbolize_names: true로 읽었다면 키를 먼저 되돌리세요.
자주 묻는 질문
최상위가 목록인 YAML도 되나요?
네. 루트 목록은 항목 클래스와, 루트가 그 클래스의 배열이라는 주석이 생성됩니다. 문서가 여러 개인 파일도 자동 모드에서는 이런 목록이 됩니다.
클래스에 타입이 없는 이유가 뭔가요?
루비에는 정적 필드 타입이 없기 때문입니다. 생성기는 이름, 중첩, 선택 키 여부를 정하고 값은 그대로 전달합니다.
선택 키는 어떻게 처리되나요?
같은 위치의 일부 매핑에 없거나 일부에서 null인 키는 nil이 기본값인 키워드 인자가 되어, initialize에서 필수 인자 뒤에 놓입니다.
병합 키도 펼치나요?
기본적으로 펼칩니다. <<: *anchor로 물려받은 키도 클래스의 attr_reader가 되고, 병합 키 펼치기를 끄면 <<라는 키에서 만들어진 속성이 그 자리에 나타납니다.
class나 to_h 같은 키는 어떻게 되나요?
속성 이름에 _value가 붙어 class_value, to_h_value가 되고, from_hash와 to_h에서는 원래 키를 그대로 씁니다.
JSON에도 같은 클래스를 쓸 수 있나요?
네. from_json은 JSON 문자열을 읽고 to_json은 JSON을 쓰므로, 같은 클래스를 YAML로 읽은 해시와 JSON 페이로드 양쪽에 쓸 수 있습니다.
YAML이 업로드되나요?
아니요. 파싱과 코드 생성은 브라우저 탭 안에서 실행되며 서버로 전송되지 않습니다. 입력은 20MiB까지입니다.
마지막 업데이트:
