Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow an item in one extension to patch an item in another extension #111

Merged
merged 3 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 50 additions & 21 deletions lib/schema/cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,9 @@ defmodule Schema.Cache do
|> put_in([:attributes, :category_uid, :_source], name)
end

# Adds :_source key to each attribute of item. This must be done before processing (compiling)
# inheritance and patching (resolve_extends and patch_type) since after that processing the
# original source is lost.
defp attribute_source({item_key, item}) do
item =
Map.update(
Expand All @@ -942,22 +945,19 @@ defmodule Schema.Cache do
{key, nil}

{key, attribute} ->
attribute = Map.put(attribute, :_source, item_key)

attribute =
if patch_extends?(item) do
# TODO: HACK. This is part of the code compensating for the
# Schema.Cache.patch_type processing. An attribute _source for an
# extension class or object that uses this patch mechanism
# keeps the form "<extension>/<name>" form, which doesn't refer to
# anything after the patch_type processing. This requires a deeper change
# to fix, so here we just keep an extra _source_patched key.
Map.put(attribute, :_source_patched, String.to_atom(patch_name(item)))
else
if patch_extends?(item) do
# Because attribute_source done before patching with patch_type, we need to
# capture the final "patched" type for use by the UI when displaying the source.
# Other uses of :_source require the original pre-patched source.
{
key,
attribute
end

{key, attribute}
|> Map.put(:_source, item_key)
|> Map.put(:_source_patched, String.to_atom(item[:extends]))
}
else
{key, Map.put(attribute, :_source, item_key)}
end
end
)
end
Expand All @@ -968,11 +968,24 @@ defmodule Schema.Cache do

defp patch_type(items, kind) do
Enum.reduce(items, %{}, fn {key, item}, acc ->
# Logger.debug(fn ->
# patching? =
# if patch_extends?(item) do
# " PATCHING:"
# else
# "not patching:"
# end

# "#{patching?} key #{inspect(key)}," <>
# " name #{inspect(item[:name])}, extends #{inspect(item[:extends])}," <>
# " caption #{inspect(item[:caption])}, extension #{inspect(item[:extension])}"
# end)

if patch_extends?(item) do
# This is an extension class or object with the same name its own base class
# (The name is not prefixed with the extension name, unlike a key / uid.)

name = patch_name(item)
name = item[:extends]

base_key = String.to_atom(name)

Expand Down Expand Up @@ -1010,14 +1023,30 @@ defmodule Schema.Cache do
end)
end

defp patch_name(item) do
item[:name] || item[:extends]
end

# Is this item a special patch extends definition as done by patch_type.
# It is triggered by a class or object that has no name or the name is the same as the extends.
defp patch_extends?(item) do
patch_name(item) == item[:extends]
extends = item[:extends]

if extends do
name = item[:name]

if name do
if name == extends do
# name and extends are the same
true
else
# true if name matches extends without extension scope ("extension/name" -> "name")
# This when an item in one extension patches an item in another.
name == Utils.descope(extends)
end
else
# extends with no name
true
end
else
false
end
end

defp patch_constraints(base, item) do
Expand Down
18 changes: 7 additions & 11 deletions lib/schema_web/views/page_view.ex
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,13 @@ defmodule SchemaWeb.PageView do
end
end

defp get_hierarchy_source(field) do
# In the case of an attribute from a patched class or object, we want to display the final
# compiled type, which is in :_source_patched and in this case :_source contains the name of
# the pre-patched item.
field[:_source_patched] || field[:_source]
end

# Build a class or object hierarchy path from item_key to target_parent_item_key.
# Returns {true, path} when a path is found, and {false, []} otherwise.
@spec build_hierarchy(atom(), atom(), map(), list()) :: {boolean(), list()}
Expand Down Expand Up @@ -261,17 +268,6 @@ defmodule SchemaWeb.PageView do
end
end

defp get_hierarchy_source(field) do
# TODO: HACK. This is part of the code compensating for the
# Schema.Cache.patch_type processing. An attribute _source for an
# extension class or object that uses this patch mechanism
# keeps the form "<extension>/<name>" form, which doesn't refer to
# anything after the patch_type processing. This requires a deeper change
# to fix, so here we just keep an extra _source_patched key.
# Use "fixed" :_source_patched if available
field[:_source_patched] || field[:_source]
end

defp format_hierarchy(path, all_items, kind) do
Enum.map(
path,
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
defmodule Schema.MixProject do
use Mix.Project

@version "2.73.0"
@version "2.73.1"

def project do
build = System.get_env("GITHUB_RUN_NUMBER") || "SNAPSHOT"
Expand Down
4 changes: 2 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
"plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"},
"plug_cowboy": {:hex, :plug_cowboy, "2.7.1", "87677ffe3b765bc96a89be7960f81703223fe2e21efa42c125fcd0127dd9d6b2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "02dbd5f9ab571b864ae39418db7811618506256f6d13b4a45037e5fe78dc5de3"},
"plug_cowboy": {:hex, :plug_cowboy, "2.7.2", "fdadb973799ae691bf9ecad99125b16625b1c6039999da5fe544d99218e662e4", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "245d8a11ee2306094840c000e8816f0cbed69a23fc0ac2bcf8d7835ae019bb2f"},
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.7", "65fa74042530064ef0570b75b43f5c49bb8b235d6515671b3d250022cb8a1f9e", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d0f478ee64deddfec64b800673fd6e0c8888b079d9f3444dd96d2a98383bdbd1"},
}
7 changes: 7 additions & 0 deletions test/test_ocsf_schema/extensions2/rpg2/extension.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"caption": "RPG 2",
"description": "The second RPG extension extends the RPG extension.",
"name": "rpg2",
"uid": 43,
"version": "0.1.0-rpg2-test"
}
10 changes: 10 additions & 0 deletions test/test_ocsf_schema/extensions2/rpg2/objects/device_(patch).json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"caption": "RPG 2 Device",
"description": "Patch extends of Device for the RPG 2 extension.",
"extends": "device",
"attributes": {
"rpg/hp": {
"requirement": "optional"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"caption": "RPG 2 Hammer Patch",
"extends": "rpg/hammer",
"attributes": {
"rpg/hp": {
"requirement": "optional"
}
}
}
20 changes: 20 additions & 0 deletions test/test_ocsf_schema/extensions2/rpg2/objects/sword.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"caption": "Sword",
"description": "The Sword object represents a stabby instrument of death.",
"name": "sword",
"attributes": {
"desc": {
"caption": "Description",
"description": "The description of the sword, ordinarily as reported by the blacksmith.",
"requirement": "optional"
},
"name": {
"description": "The sword's name. Named swords are just cooler.",
"requirement": "recommended"
},
"rpg/damage_range": {
"description": "The amount of damage that could be inflicted by this sword.",
"requirement": "recommended"
}
}
}
Loading