|
Mix.install([ |
|
{:req, "~> 0.3.6"}, |
|
{:inflex, "~> 2.0.0"} |
|
]) |
|
|
|
defmodule EnumParser do |
|
defstruct name: nil, |
|
stack: [] |
|
|
|
def push(state, token, opts \\ []) do |
|
%EnumParser{ |
|
name: Keyword.get(opts, :name, state.name), |
|
stack: [token | state.stack] |
|
} |
|
end |
|
|
|
def parse( |
|
%{ "kind" => "identifier", "text" => name } = token, |
|
%EnumParser{ :stack => [ |
|
%{ "kind" => "text", "text" => " " }, |
|
%{ "kind" => "keyword", "text" => "case" } |
|
| _ |
|
] } = state |
|
) do |
|
push(state, token, name: String.replace(name, "`", "")) |
|
end |
|
|
|
def parse( |
|
%{ "kind" => "identifier", "text" => name } = token, |
|
%EnumParser{ :stack => [ |
|
%{ "kind" => "text", "text" => " " }, |
|
%{ "kind" => "keyword", "text" => "let" }, |
|
%{ "kind" => "text", "text" => " " }, |
|
%{ "kind" => "keyword", "text" => "static" } |
|
| _ |
|
] } = state |
|
) do |
|
push(state, token, name: String.replace(name, "`", "")) |
|
end |
|
|
|
def parse( |
|
%{ "kind" => "identifier", "text" => name } = token, |
|
%EnumParser{ :stack => [ |
|
%{ "kind" => "text", "text" => " " }, |
|
%{ "kind" => "keyword", "text" => "var" }, |
|
%{ "kind" => "text", "text" => " " }, |
|
%{ "kind" => "keyword", "text" => "static" } |
|
| _ |
|
] } = state |
|
) do |
|
push(state, token, name: String.replace(name, "`", "")) |
|
end |
|
|
|
def parse(token, state) do |
|
push(state, token) |
|
end |
|
end |
|
|
|
defmodule TypeReducer do |
|
def reduce_type(%{ "kind" => "typeIdentifier", "text" => type }, {name, path}) do |
|
{:cont, {name, [type | path]}} |
|
end |
|
def reduce_type(%{ "kind" => "text", "text" => "." }, acc) do |
|
{:cont, acc} |
|
end |
|
def reduce_type(%{ "kind" => "text", "text" => <<":", _::binary>> }, acc) do |
|
{:cont, acc} |
|
end |
|
def reduce_type(%{ "kind" => "internalParam", "text" => name }, {_, path}) do |
|
{:cont, { name, path }} |
|
end |
|
def reduce_type(%{ "kind" => "externalParam", "text" => name }, {acc_name, path}) do |
|
{:cont, { (if name == nil, do: acc_name, else: name), path }} |
|
end |
|
def reduce_type(_, acc) do |
|
{:halt, acc} |
|
end |
|
end |
|
|
|
defmodule ModifierParser do |
|
defstruct name: "", |
|
params: [], |
|
generics: %{}, |
|
stack: [] |
|
|
|
def push(state, token, opts \\ []) do |
|
%ModifierParser{ |
|
name: Keyword.get(opts, :name, state.name), |
|
params: Keyword.merge(state.params, Keyword.get(opts, :params, [])), |
|
generics: Map.merge(state.generics, Keyword.get(opts, :generics, %{})), |
|
stack: [token | state.stack] |
|
} |
|
end |
|
|
|
# Name |
|
|
|
def parse( |
|
%{ "kind" => "identifier", "text" => name } = token, |
|
%ModifierParser{ :stack => [%{ "kind" => "text" }, %{ "kind" => "keyword", "text" => "func" } | _] } = state |
|
) do |
|
push(state, token, name: name) |
|
end |
|
|
|
# Parameters |
|
|
|
def parse( |
|
%{ "kind" => "typeIdentifier", "text" => type, "identifier" => identifier } = token, |
|
%ModifierParser{ :stack => [%{ "kind" => "text", "text" => "." } | _] } = state |
|
) do |
|
link = "https://developer.apple.com/tutorials/data#{String.trim_leading(identifier, "doc://com.apple.SwiftUI")}.json" |
|
case Enum.reduce_while(state.stack, {nil, [type]}, &TypeReducer.reduce_type/2) do |
|
{nil, _} -> |
|
push(state, token) |
|
{param, path} -> |
|
push(state, token, params: [{String.to_atom(param), {Enum.join(path, "."), link}}]) |
|
end |
|
end |
|
|
|
def parse( |
|
%{ "kind" => "typeIdentifier", "text" => type, "identifier" => identifier } = token, |
|
%ModifierParser{ :stack => [%{ "kind" => "text", "text" => ": " }, %{ "kind" => "internalParam", "text" => name } | _] } = state |
|
) do |
|
link = "https://developer.apple.com/tutorials/data#{String.trim_leading(identifier, "doc://com.apple.SwiftUI")}.json" |
|
push(state, token, params: [{String.to_atom(name), {type, link}}]) |
|
end |
|
|
|
def parse( |
|
%{ "kind" => "typeIdentifier", "text" => type, "identifier" => identifier } = token, |
|
%ModifierParser{ :stack => [%{ "kind" => "text", "text" => <<":", _::binary>> }, %{ "kind" => "externalParam", "text" => name } | _] } = state |
|
) do |
|
link = "https://developer.apple.com/tutorials/data#{String.trim_leading(identifier, "doc://com.apple.SwiftUI")}.json" |
|
push(state, token, params: [{String.to_atom(name), {type, link}}]) |
|
end |
|
|
|
def parse( |
|
%{ "kind" => "typeIdentifier", "text" => type } = token, |
|
%ModifierParser{ :stack => [%{ "kind" => "text", "text" => ": " }, %{ "kind" => "internalParam", "text" => name } | _] } = state |
|
) do |
|
push(state, token, params: [{String.to_atom(name), type}]) |
|
end |
|
|
|
def parse( |
|
%{ "kind" => "typeIdentifier", "text" => type } = token, |
|
%ModifierParser{ :stack => [%{ "kind" => "text", "text" => <<":", _::binary>> }, %{ "kind" => "externalParam", "text" => name } | _] } = state |
|
) do |
|
push(state, token, params: [{String.to_atom(name), type}]) |
|
end |
|
|
|
# Generic Parameters |
|
|
|
def parse( |
|
%{ "kind" => "typeIdentifier", "text" => type } = token, |
|
%ModifierParser{ :stack => [%{ "kind" => "text", "text" => " : " }, %{ "kind" => "typeIdentifier", "text" => name } | _] } = state |
|
) do |
|
push(state, token, generics: %{ name => type }) |
|
end |
|
|
|
# Catch-all |
|
|
|
def parse(token, state) do |
|
push(state, token) |
|
end |
|
|
|
# Conversions |
|
|
|
def to_type(_, {"Color", _}), do: "Color" |
|
def to_type(_, "String"), do: ":string" |
|
def to_type(_, "Int"), do: ":integer" |
|
def to_type(_, "Double"), do: ":float" |
|
def to_type(_, "Bool"), do: ":boolean" |
|
def to_type(_, "View"), do: ":string" |
|
def to_type(_, "Alignment"), do: "Ecto.Enum, values: ~w(bottom bottom_leading bottom_trailing center leading leading_last_text_baseline top top_leading top_trailing trailing trailing_first_text_baseline)a" |
|
def to_type(state, {type, link}) do |
|
cases = get_cases(link) |
|
if Enum.empty?(cases) do |
|
to_type(state, type) |
|
else |
|
"Ecto.Enum, values: ~w(#{cases |> Enum.map(fn c -> Inflex.underscore(c) end) |> Enum.join(" ")})a" |
|
end |
|
end |
|
def to_type(state, type) do |
|
if Map.has_key?(state.generics, type) do |
|
to_type(state, state.generics[type]) |
|
else |
|
type |
|
end |
|
end |
|
def get_cases(link) do |
|
if String.contains?(link, "doc://com.externally.resolved.symbol") do |
|
[] |
|
else |
|
Req.get!(link).body |
|
|> Map.get("references") |
|
|> Map.values() |
|
|> Enum.filter(fn ref -> Map.has_key?(ref, "fragments") end) |
|
|> Enum.map(fn ref -> Enum.reduce(ref["fragments"], %EnumParser{}, &EnumParser.parse/2).name end) |
|
|> Enum.reject(&is_nil/1) |
|
end |
|
end |
|
|
|
def to_field(state, { name, type }), do: "field :#{Inflex.underscore(name)}, #{to_type(state, type)}" |
|
|
|
def to_property(_, { name, {type, _} }), do: "private let #{name}: #{type}" |
|
def to_property(_, { name, type }), do: "private let #{name}: #{type}" |
|
|
|
def to_decode(state, { name, {"Color", _} }), do: to_decode(state, {name, "Color"}) |
|
def to_decode(state, { name, {type, link} }) do |
|
cases = get_cases(link) |
|
if Enum.empty?(cases) do |
|
to_decode(state, {name, type}) |
|
else |
|
""" |
|
switch try container.decode(String.self, forKey: .#{name}) { |
|
#{cases |> Enum.map(fn c -> "case \"#{Inflex.underscore(c)}\": self.#{name} = .#{c}" end) |> Enum.join("\n ")} |
|
case let `default`: throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "unknown #{name} '\\(`default`)'")) |
|
} |
|
""" |
|
end |
|
end |
|
def to_decode(_, { name, type }), do: "self.#{name} = try container.decode(#{type}.self, forKey: .#{name})" |
|
|
|
def to_codingkey(_, { name, _ }) do |
|
underscored = Inflex.underscore(name) |
|
if Atom.to_string(name) == underscored do |
|
"case #{name}" |
|
else |
|
"case #{name} = \"#{underscored}\"" |
|
end |
|
end |
|
|
|
def to_schema(state) do |
|
""" |
|
defmodule LiveViewNativeSwiftUi.Modifiers.#{Inflex.camelize(state.name)} do |
|
use LiveViewNativePlatform.Modifier |
|
|
|
modifier_schema "#{Inflex.underscore(state.name)}" do |
|
#{Enum.map(state.params, fn p -> to_field(state, p) end) |> Enum.join("\n ")} |
|
end |
|
end |
|
""" |
|
end |
|
|
|
def to_modifier(state) do |
|
date = Date.utc_today() |
|
""" |
|
// |
|
// #{Inflex.camelize(state.name)}Modifier.swift |
|
// LiveViewNative |
|
// |
|
// Created by <#Name#> on #{date.month}/#{date.day}/#{date.year}. |
|
// |
|
|
|
import SwiftUI |
|
|
|
/// <#Documentation#> |
|
#if swift(>=5.8) |
|
@_documentation(visibility: public) |
|
#endif |
|
struct #{Inflex.camelize(state.name)}Modifier<R: RootRegistry>: ViewModifier, Decodable { |
|
/// <#Documentation#> |
|
#if swift(>=5.8) |
|
@_documentation(visibility: public) |
|
#endif |
|
#{Enum.map(state.params, fn p -> to_property(state, p) end) |> Enum.join( |
|
~s""" |
|
|
|
|
|
/// <#Documentation#> |
|
#if swift(>=5.8) |
|
@_documentation(visibility: public) |
|
#endif |
|
""" <> " " |
|
)} |
|
|
|
init(from decoder: Decoder) throws { |
|
let container = try decoder.container(keyedBy: CodingKeys.self) |
|
|
|
#{Enum.map(state.params, fn p -> to_decode(state, p) end) |> Enum.join("\n ")} |
|
} |
|
|
|
func body(content: Content) -> some View { |
|
content.#{state.name}( |
|
<#Arguments#> |
|
) |
|
} |
|
|
|
enum CodingKeys: String, CodingKey { |
|
#{Enum.map(state.params, fn p -> to_codingkey(state, p) end) |> Enum.join("\n ")} |
|
} |
|
} |
|
""" |
|
end |
|
end |
|
|
|
defmodule Generator do |
|
def generate() do |
|
{args, _, _} = System.argv() |> OptionParser.parse(strict: [url: :string, schema: :boolean, modifier: :boolean]) |
|
|
|
url = Keyword.get(args, :url) |
|
|
|
url = URI.parse(url) |
|
declaration = Req.get!("https://developer.apple.com/tutorials/data#{url.path}.json").body |
|
|> Map.get("primaryContentSections") |
|
|> Enum.find(nil, fn section -> section["kind"] == "declarations" end) |
|
|> Map.get("declarations") |
|
|> hd() |
|
|> Map.get("tokens") |
|
state = Enum.reduce(declaration, %ModifierParser{}, &ModifierParser.parse/2) |
|
if Keyword.get(args, :schema, false) do |
|
state |
|
|> ModifierParser.to_schema() |
|
|> IO.puts |
|
end |
|
if Keyword.get(args, :modifier, false) do |
|
state |
|
|> ModifierParser.to_modifier() |
|
|> IO.puts |
|
end |
|
end |
|
end |
|
|
|
Generator.generate() |