···11-# frozen_string_literal: true
22-33-module Literal::Attributable
44- autoload :Generators, "literal/attributable/generators"
55- autoload :Nodes, "literal/attributable/nodes"
66- autoload :Formatter, "literal/attributable/formatter"
77-88- include Literal::Types
99-1010- VisibilityOptions = Set[false, :private, :protected, :public].freeze
1111- KindOptions = Set[nil, :positional, :*, :**, :&].freeze
1212-1313- def attribute(name, type, kind = nil, reader: false, writer: false, default: nil, &coercion)
1414- if default && !(Proc === default || default.frozen?)
1515- raise Literal::ArgumentError.new("The default must be a frozen object or a Proc.")
1616- end
1717-1818- unless VisibilityOptions.include?(reader)
1919- raise Literal::ArgumentError.new("The reader must be one of #{VisibilityOptions.map(&:inspect).join(', ')}.")
2020- end
2121-2222- unless VisibilityOptions.include?(writer)
2323- raise Literal::ArgumentError.new("The writer must be one of #{VisibilityOptions.map(&:inspect).join(', ')}.")
2424- end
2525-2626- if reader && :class == name
2727- raise Literal::ArgumentError.new(
2828- "The `:class` attribute should not be defined as a reader because it breaks Ruby's `Object#class` method, which Literal itself depends on.",
2929- )
3030- end
3131-3232- unless KindOptions.include?(kind)
3333- raise Literal::ArgumentError.new("The kind must be one of #{KindOptions.map(&:inspect).join(', ')}.")
3434- end
3535-3636- attribute = Literal::Attribute.new(
3737- name:,
3838- type:,
3939- kind:,
4040- reader:,
4141- writer:,
4242- default:,
4343- coercion:,
4444- )
4545-4646- literal_attributes[name] = attribute
4747- define_literal_methods(attribute)
4848- include literal_extension
4949- end
5050-5151- def inherited(subclass)
5252- literal_attributes = self.literal_attributes
5353-5454- subclass.instance_exec do
5555- @literal_attributes = literal_attributes.dup
5656- end
5757-5858- super
5959- end
6060-6161- def literal_attributes
6262- return @literal_attributes if defined?(@literal_attributes)
6363-6464- if superclass.is_a?(Literal::Attributable)
6565- @literal_attributes = superclass.literal_attributes.dup
6666- else
6767- @literal_attributes = Literal::ConcurrentHash.new
6868- end
6969- end
7070-7171- def define_literal_methods(attribute)
7272- literal_extension.module_eval <<~RUBY, __FILE__, __LINE__ + 1
7373- # frozen_string_literal: true
7474-7575- #{generate_literal_initializer}
7676-7777- #{generate_literal_reader(attribute) if attribute.reader}
7878-7979- #{generate_literal_writer(attribute) if attribute.writer}
8080- RUBY
8181- end
8282-8383- def literal_extension
8484- defined?(@literal_extension) ? @literal_extension : @literal_extension = Module.new
8585- end
8686-end
-134
lib/literal/attributable/formatter.rb
···11-# frozen_string_literal: true
22-33-class Literal::Attributable::Formatter < Literal::Formatter
44- def Access(node)
55- visit node.collection
66- text "["
77- visit node.key
88- text "]"
99- end
1010-1111- def Assignment(node)
1212- visit node.left
1313- text " = "
1414- visit node.right
1515- end
1616-1717- def AssignSchema(node)
1818- text "@literal_attributes = self.class.literal_attributes"
1919- end
2020-2121- def AttributeCoercion(node)
2222- text "#{node.attribute.name} = @literal_attributes[:#{node.attribute.name}].coerce(#{node.attribute.name}, context: self)"
2323- end
2424-2525- def BlockParam(node)
2626- text "&#{node.attribute.name}"
2727- end
2828-2929- def Def(node)
3030- text "#{node.visibility} " if node.visibility
3131- text "def #{node.name}"
3232- if node.params&.any?
3333- text "("
3434- visit_each(node.params) { text ", " }
3535- text ")"
3636- end
3737-3838- indent do
3939- visit_each(node.body) { newline; newline }
4040- end
4141-4242- newline
4343- text "end"
4444- newline
4545- end
4646-4747- def DefaultAssignment(node)
4848- text "if Literal::Null == #{node.attribute.escaped_name}"
4949-5050- indent do
5151- text "#{node.attribute.escaped_name} = @literal_attributes[:#{node.attribute.name}].default_value"
5252- end
5353-5454- newline
5555- text "end"
5656- end
5757-5858- def HashLiteral(node)
5959- text "{"
6060-6161- indent do
6262- visit_each(node.mappings) { text ","; newline }
6363- end
6464-6565- newline
6666-6767- text "}"
6868- end
6969-7070- def InitializerCallback(node)
7171- comment "Callback for your own setup logic"
7272- text "after_initialization if respond_to?(:after_initialization)"
7373- end
7474-7575- def KeywordEscape(node)
7676- text "#{node.attribute.escaped_name} = binding.local_variable_get(:#{node.attribute.name})"
7777- end
7878-7979- def KeywordParam(node)
8080- if node.attribute.default
8181- text "#{node.attribute.name}: Literal::Null"
8282- elsif node.attribute.type === nil
8383- text "#{node.attribute.name}: nil"
8484- else
8585- text "#{node.attribute.name}:"
8686- end
8787- end
8888-8989- def KeywordSplat(node)
9090- text "**#{node.attribute.name}"
9191- end
9292-9393- def Mapping(node)
9494- visit node.left
9595- text " => "
9696- visit node.right
9797- end
9898-9999- def PositionalParam(node)
100100- if node.default
101101- text "#{node.name} = #{node.default}"
102102- else
103103- text node.name
104104- end
105105- end
106106-107107- def PositionalSplat(node)
108108- text "*#{node.attribute.name}"
109109- end
110110-111111- def Ref(node)
112112- text node.name
113113- end
114114-115115- def Section(node)
116116- comment node.name if node.name
117117- visit_each(node.body) { newline; newline } if node.body
118118- end
119119-120120- def Symbol(node)
121121- text ":#{node.name}"
122122- end
123123-124124- def TypeCheck(node)
125125- text "unless @literal_attributes[:#{node.attribute_name}].type === #{node.variable_name}"
126126-127127- indent do
128128- text "raise Literal::TypeError.expected(#{node.variable_name}, to_be_a: @literal_attributes[:#{node.attribute_name}].type)"
129129- end
130130-131131- newline
132132- text "end"
133133- end
134134-end
···11-# frozen_string_literal: true
22-33-module Literal::Attributable::Generators
44- class Base
55- include Literal::Attributable::Nodes
66-77- def call
88- Literal::Attributable::Formatter.new.visit(template)
99- end
1010- end
1111-end
···11-# frozen_string_literal: true
22-33-class Literal::Attribute
44- RUBY_KEYWORDS = %i[alias and begin break case class def do else elsif end ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield].to_h { |k| [k, "__#{k}__"] }.freeze
55-66- def initialize(name:, type:, kind:, reader:, writer:, default:, coercion:)
77- @name = name
88- @type = type
99- @kind = kind
1010- @reader = reader
1111- @writer = writer
1212- @default = default
1313- @coercion = coercion
1414- end
1515-1616- attr_reader :name, :type, :kind, :reader, :writer, :default, :coercion
1717-1818- def coerce(value, context:)
1919- context.instance_exec(value, &@coercion)
2020- end
2121-2222- def ruby_keyword?
2323- !!RUBY_KEYWORDS[@name]
2424- end
2525-2626- def escaped_name
2727- RUBY_KEYWORDS[@name] || @name
2828- end
2929-3030- def default_value
3131- case @default
3232- when Proc then @default.call
3333- else @default
3434- end
3535- end
3636-end
-19
lib/literal/attributes.rb
···11-# frozen_string_literal: true
22-33-module Literal::Attributes
44- include Literal::Attributable
55-66- private
77-88- def generate_literal_initializer
99- Generators::IVarInitializer.new(literal_attributes).call
1010- end
1111-1212- def generate_literal_writer(attribute)
1313- Generators::IVarWriter.new(attribute).call
1414- end
1515-1616- def generate_literal_reader(attribute)
1717- Generators::IVarReader.new(attribute).call
1818- end
1919-end
···11-# frozen_string_literal: true
22-33-# @api private
44-class Literal::Visitor
55- def visit(node)
66- node.accept(self)
77- end
88-99- def visit_each(nodes)
1010- total = nodes.size
1111- i = 0
1212- while i < total
1313- visit(nodes[i])
1414- i += 1
1515- yield if i < total
1616- end
1717- end
1818-end