Compare commits
87 Commits
v2.5.2
...
388822e90a
Author | SHA1 | Date | |
---|---|---|---|
388822e90a | |||
91993b4548 | |||
bb69558aaa | |||
052c670627 | |||
67fd215d0e | |||
9ecbbff6fa | |||
774ac0f0ca | |||
b785d24a08 | |||
22f991e72e | |||
5518b27663 | |||
0b899dea2c | |||
3424fea8ad | |||
ddc1d83d58 | |||
4b0a85411d | |||
46e871b626 | |||
258dcc88e7 | |||
75bf449bed | |||
58586395fb | |||
c5a68af5e6 | |||
b4c0284734 | |||
c5d1dad8de | |||
4ff2ee80ee | |||
633eebfd2a | |||
5a31703840 | |||
162d0c758d | |||
14d64495b6 | |||
fe6e97e832 | |||
35b3d8b099 | |||
2e3e958e15 | |||
955afc4295 | |||
2c487bc443 | |||
b77224176b | |||
a2201053c5 | |||
04cedf5ece | |||
ebb07854cc | |||
8a86ae2f40 | |||
e8f16dda2b | |||
513773f641 | |||
22914fe243 | |||
2d523dfe64 | |||
2629722f67 | |||
1f6c4e4976 | |||
bfd08e754e | |||
750010b71a | |||
9064a53820 | |||
294c04a11a | |||
ba7ac07001 | |||
5d10178bf9 | |||
f91c2b4795 | |||
057db23d09 | |||
bf72734b90 | |||
cc30c2bdcb | |||
f453079c72 | |||
e634fe28bd | |||
4e4b7bbd19 | |||
89eed3f847 | |||
f008efd5e1 | |||
f6def1e5a5 | |||
867b188718 | |||
aac29a4074 | |||
8a40f463f7 | |||
8d4db1da91 | |||
d41e2afe17 | |||
76457d22cf | |||
912950d463 | |||
25326ea11b | |||
df212b7fcc | |||
f4a963760a | |||
d236811cb9 | |||
da93770334 | |||
d9f54a8354 | |||
dc8da8ab63 | |||
24262a7dca | |||
d77b13c363 | |||
a9c60a3698 | |||
66bcf21d79 | |||
e847e5c3ce | |||
9a70c9696e | |||
9cea103042 | |||
81d8259dfc | |||
5c5fbac63f | |||
3e818e61c7 | |||
001470ffe4 | |||
d88a76c4e2 | |||
d3a1f1bd96 | |||
07a5f3f1a4 | |||
e2257e082a |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
*.exe
|
||||
.qodo
|
||||
*.sqlite
|
||||
|
81
.vscode/launch.json
vendored
81
.vscode/launch.json
vendored
@@ -5,16 +5,87 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Package",
|
||||
"name": "Launch Package (Barotrauma)",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"cwd": "C:/Users/Administrator/Seafile/Games-Barotrauma",
|
||||
"args": [
|
||||
"-loglevel",
|
||||
"trace",
|
||||
"-cook",
|
||||
"*.yml",
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Launch Package (Payday 2)",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"cwd": "C:/Users/Administrator/Seafile/Games-Payday2",
|
||||
"args": [
|
||||
"-loglevel",
|
||||
"trace",
|
||||
"*.yml",
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Launch Package (Barotrauma cookfile)",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"cwd": "C:/Users/Administrator/Seafile/Games-Barotrauma",
|
||||
"args": [
|
||||
"-loglevel",
|
||||
"trace",
|
||||
"-cook",
|
||||
"cookassistant.yml",
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Launch Package (Quasimorph cookfile)",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"cwd": "C:/Users/Administrator/Seafile/Games-Quasimorph",
|
||||
"args": [
|
||||
"cook.yml",
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Launch Package (Rimworld cookfile)",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"cwd": "C:/Users/Administrator/Seafile/Games-Rimworld/294100",
|
||||
"args": [
|
||||
"cookVehicles.yml",
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Launch Package (Workspace)",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"args": [
|
||||
"-mode=json",
|
||||
"$..name",
|
||||
"v='pero'",
|
||||
"test.json"
|
||||
"tester.yml",
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Launch Package (Avorion)",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${workspaceFolder}",
|
||||
"cwd": "C:/Users/Administrator/Seafile/Games-Avorion/Avorion",
|
||||
"args": [
|
||||
"*.yml",
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@@ -1,651 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Talents>
|
||||
<Talent identifier="powerarmor">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="5,6" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.powerarmor">
|
||||
<Replace tag="[bonusmovement]" value="25" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.unlockrecipe">
|
||||
<Replace tag="[itemname]" value="entityname.exosuit" color="gui.orange"/>
|
||||
</Description>
|
||||
<AbilityGroupInterval interval="0.9">
|
||||
<Conditions>
|
||||
<AbilityConditionHasItem tags="deepdivinglarge" />
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityModifyStat stattype="MovementSpeed" value="0.25" />
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
<AddedRecipe itemidentifier="exosuit"/>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="foolhardy">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="4,6" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.lowhealthstatboost">
|
||||
<Replace tag="[health]" value="50" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.additionalstattype">
|
||||
<Replace tag="[amount]" value="20" color="gui.green"/>
|
||||
<Replace tag="[stattype]" value="stattypenames.physicalresistance" color="gui.orange"/>
|
||||
</Description>
|
||||
<AbilityGroupInterval interval="0.9">
|
||||
<Conditions>
|
||||
<AbilityConditionAboveVitality invert="true" vitalitypercentage="0.5"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityApplyStatusEffects>
|
||||
<StatusEffects>
|
||||
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true" multiplyafflictionsbymaxvitality="true">
|
||||
<Affliction identifier="foolhardy" amount="1.0"/>
|
||||
</StatusEffect>
|
||||
</StatusEffects>
|
||||
</CharacterAbilityApplyStatusEffects>
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="berserker">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="3,6" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.lowhealthstatboost">
|
||||
<Replace tag="[health]" value="50" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.additionalstattype">
|
||||
<Replace tag="[amount]" value="20" color="gui.green"/>
|
||||
<Replace tag="[stattype]" value="stattypenames.meleedamagebonus" color="gui.orange"/>
|
||||
</Description>
|
||||
<AbilityGroupInterval interval="0.9">
|
||||
<Conditions>
|
||||
<AbilityConditionAboveVitality invert="true" vitalitypercentage="0.5"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityApplyStatusEffects>
|
||||
<StatusEffects>
|
||||
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true" multiplyafflictionsbymaxvitality="true">
|
||||
<Affliction identifier="berserker" amount="1.0"/>
|
||||
</StatusEffect>
|
||||
</StatusEffects>
|
||||
</CharacterAbilityApplyStatusEffects>
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="mudraptorwrestler">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="2,6" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.mudraptorwrestler">
|
||||
<Replace tag="[amount]" value="50" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.additionalstattypeself">
|
||||
<Replace tag="[amount]" value="10" color="gui.green"/>
|
||||
<Replace tag="[stattype]" value="stattypenames.physicalresistance" color="gui.orange"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="OnAttack">
|
||||
<Conditions>
|
||||
<AbilityConditionAttackData weapontype="NoWeapon,Melee" />
|
||||
<AbilityConditionCharacter>
|
||||
<Conditional group="eq mudraptor" />
|
||||
</AbilityConditionCharacter>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityModifyAttackData addeddamagemultiplier="0.5"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveResistance resistanceid="damage" multiplier="0.9"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="heavylifting">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="1,6" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.heavylifting">
|
||||
<Replace tag="[amount]" value="20" color="gui.green"/>
|
||||
</Description>
|
||||
<AbilityGroupInterval interval="0.9">
|
||||
<Conditions>
|
||||
<AbilityConditionHoldingItem tags="alienartifact,crate"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityModifyStat stattype="MovementSpeed" value="0.2"/>
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="iamthatguy">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="0,6" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.iamthatguy">
|
||||
<Replace tag="[amount]" value="20" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.skillbonus">
|
||||
<Replace tag="[amount]" value="20" color="gui.green"/>
|
||||
<Replace tag="[skillname]" value="stattypenames.weaponsskillbonus" color="gui.orange"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.unlockrecipe">
|
||||
<Replace tag="[itemname]" value="entityname.heavywrench" color="gui.orange"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveStat stattype="WeaponsSkillBonus" value="20"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<AbilityGroupEffect abilityeffecttype="OnAddDamageAffliction">
|
||||
<Abilities>
|
||||
<CharacterAbilityModifyAffliction afflictionidentifiers="blunttrauma" addedmultiplier="0.2" />
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<AddedRecipe itemidentifier="heavywrench"/>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="robotics">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="3,7" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.robotics"/>
|
||||
<Description tag="talentdescription.roboticsreminder">
|
||||
<Replace tag="[amount]" value="2" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.unlockrecipe">
|
||||
<Replace tag="[itemname]" value="entityname.defensebotspawner,entityname.defensebotammobox" color="gui.orange"/>
|
||||
</Description>
|
||||
<AddedRecipe itemidentifier="defensebotspawner"/>
|
||||
<AddedRecipe itemidentifier="defensebotammobox"/>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="ironstorm">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="7,5" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.ironstorm">
|
||||
<Replace tag="[chance]" value="10" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.unlockrecipe">
|
||||
<Replace tag="[itemname]" value="entityname.scrapcannon" color="gui.orange"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilitySetMetadataInt identifier="tiermodifieroverride" value="3"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<AddedRecipe itemidentifier="scrapcannon"/>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="residualwaste">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="6,5" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.residualwaste">
|
||||
<Replace tag="[chance]" value="20" color="gui.green"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="OnItemDeconstructedMaterial">
|
||||
<Conditions>
|
||||
<AbilityConditionServerRandom randomChance="0.2"/>
|
||||
<!-- don't allow duplicating genetic materials, and prevent infinite FPGA circuits -->
|
||||
<AbilityConditionItem tags="geneticmaterial,unidentifiedgeneticmaterial,circuitboxcomponent,lightcomponent" invert="true"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityModifyValue multiplyvalue="2"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="massproduction">
|
||||
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="6,1" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.massproduction">
|
||||
<Replace tag="[chance]" value="40" color="gui.green"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="OnItemFabricatedIngredients">
|
||||
<Conditions>
|
||||
<AbilityConditionServerRandom randomChance="0.4" />
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityRemoveRandomIngredient>
|
||||
<AbilityConditionItem category="Material"/>
|
||||
</CharacterAbilityRemoveRandomIngredient>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="toolmaintenance">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="5,5" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.toolmaintenance">
|
||||
<Replace tag="[amount]" value="1" color="gui.green"/>
|
||||
</Description>
|
||||
<!-- Give once when unlocking the talent -->
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityGivePermanentStat statidentifier="tool~toolmaintenance" stattype="IncreaseFabricationQuality" value="1" targetallies="true" setvalue="true"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<!-- Give every 60 seconds for late comers -->
|
||||
<AbilityGroupInterval interval="60">
|
||||
<Abilities>
|
||||
<CharacterAbilityGivePermanentStat statidentifier="tool~toolmaintenance" stattype="IncreaseFabricationQuality" value="1" targetallies="true" setvalue="true"/>
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="miner">
|
||||
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="2,3" sheetelementsize="428,428"/>
|
||||
<Description tag="talentdescription.miner">
|
||||
<Replace tag="[probability]" value="320" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.gainoredetachspeed">
|
||||
<Replace tag="[amount]" value="1600" color="gui.green"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveStat stattype="RepairToolDeattachTimeMultiplier" value="1"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<AbilityGroupEffect abilityeffecttype="OnItemDeconstructedMaterial">
|
||||
<Conditions>
|
||||
<AbilityConditionServerRandom randomchance="12.8"/>
|
||||
<AbilityConditionItem tags="ore"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityModifyValue multiplyvalue="2"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="retrofit">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="3,5" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.retrofit" />
|
||||
<Description tag="talentdescription.doesnotstack" />
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilitySetMetadataInt identifier="tiermodifiers.increasewallhealth" value="1"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="ironman">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="6,6" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.unlockrecipe">
|
||||
<Replace tag="[itemname]" value="entityname.ironhelmet,entityname.makeshiftarmor" color="gui.orange"/>
|
||||
</Description>
|
||||
<AddedRecipe itemidentifier="ironhelmet"/>
|
||||
<AddedRecipe itemidentifier="makeshiftarmor"/>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="oiledmachinery">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="4,5" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.oiledmachinery">
|
||||
<Replace tag="[amount]" value="50" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.doesnotstack" />
|
||||
<AbilityGroupInterval interval="60">
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveItemStatToTags tags="fabricator" stattype="FabricationSpeed" value="1.5" />
|
||||
<CharacterAbilityGiveItemStatToTags tags="deconstructor" stattype="DeconstructorSpeed" value="1.5" />
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveItemStatToTags tags="fabricator" stattype="FabricationSpeed" value="1.5" />
|
||||
<CharacterAbilityGiveItemStatToTags tags="deconstructor" stattype="DeconstructorSpeed" value="1.5" />
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="pumpndump">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="1,7" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.pumpndump">
|
||||
<Replace tag="[amount]" value="10" color="gui.green"/>
|
||||
<Replace tag="[stattype]" value="stattypenames.maxflow" color="gui.orange"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
|
||||
<conditions>
|
||||
<AbilityConditionItem tags="pump"/>
|
||||
</conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveItemStat stattype="PumpSpeed" value="1.1"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="ballastdenizen">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="7,6" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.ballastdenizen">
|
||||
<Replace tag="[amount]" value="50" color="gui.green"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveStat stattype="HoldBreathMultiplier" value="0.5"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="engineengineer">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="2,5" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.engineengineer">
|
||||
<Replace tag="[amount]" value="2.5" color="gui.green"/>
|
||||
<Replace tag="[max]" value="5" color="gui.green"/>
|
||||
<Replace tag="[stattype]" value="stattypenames.maxspeed" color="gui.orange"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.doesnotstack" />
|
||||
<AbilityGroupInterval interval="60">
|
||||
<Conditions>
|
||||
<AbilityConditionHasLevel levelequals="1" />
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.025" />
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
<AbilityGroupInterval interval="60">
|
||||
<Conditions>
|
||||
<AbilityConditionHasLevel levelequals="2" />
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.05" />
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
<AbilityGroupInterval interval="60">
|
||||
<Conditions>
|
||||
<AbilityConditionHasLevel levelequals="3" />
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.075" />
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
<AbilityGroupInterval interval="60">
|
||||
<Conditions>
|
||||
<AbilityConditionHasLevel levelequals="4" />
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.1" />
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
<AbilityGroupInterval interval="60">
|
||||
<Conditions>
|
||||
<AbilityConditionHasLevel levelequals="5" />
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.125" />
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
<AbilityGroupInterval interval="60">
|
||||
<Conditions>
|
||||
<AbilityConditionHasLevel levelequals="6" />
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.15" />
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
<AbilityGroupInterval interval="60">
|
||||
<Conditions>
|
||||
<AbilityConditionHasLevel levelequals="7" />
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.175" />
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
<AbilityGroupInterval interval="60">
|
||||
<Conditions>
|
||||
<AbilityConditionHasLevel minlevel="8" />
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveItemStatToTags tags="engine" stattype="EngineMaxSpeed" stackable="false" value="1.2" />
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="multifunctional">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="6,1" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.multifunctional">
|
||||
<Replace tag="[powerincrease]" value="50" color="gui.green"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="OnAttack">
|
||||
<Conditions>
|
||||
<AbilityConditionAttackData tags="wrenchitem"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityModifyAttackData addeddamagemultiplier="0.5"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<AbilityGroupEffect abilityeffecttype="OnAttack">
|
||||
<Conditions>
|
||||
<AbilityConditionAttackData tags="crowbaritem"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityModifyAttackData addeddamagemultiplier="0.5"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="salvagecrew">
|
||||
<Icon texture="Content/UI/TalentsIcons3.png" sheetindex="0,7" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.bonusxponmission">
|
||||
<Replace tag="[xpbonus]" value="30" color="gui.green"/>
|
||||
<Replace tag="[missiontype]" value="missiontype.salvage" color="gui.orange"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.salvagecrew">
|
||||
<Replace tag="[swimbonus]" value="50" color="gui.green"/>
|
||||
<Replace tag="[resistanceamount]" value="10" color="gui.green"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="OnGainMissionExperience">
|
||||
<Conditions>
|
||||
<AbilityConditionMission missiontype="Salvage"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityModifyValue multiplyvalue="1.3"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<AbilityGroupInterval interval="0.9">
|
||||
<Conditions>
|
||||
<AbilityConditionInSubmarine submarinetype="Wreck" />
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityApplyStatusEffects>
|
||||
<StatusEffects>
|
||||
<StatusEffect type="OnAbility" target="This" disabledeltatime="true">
|
||||
<Affliction identifier="salvagecrew" amount="1.0"/>
|
||||
</StatusEffect>
|
||||
</StatusEffects>
|
||||
</CharacterAbilityApplyStatusEffects>
|
||||
</Abilities>
|
||||
</AbilityGroupInterval>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="machinemaniac" trackedstat="machinemaniac_counter" trackedmax="100">
|
||||
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="3,2" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.machinemaniac">
|
||||
<Replace tag="[bonus]" value="80" color="gui.green"/>
|
||||
<Replace tag="[amount]" value="3" color="gui.orange"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.machinemaniac.30">
|
||||
<Replace tag="[requirement]" value="12" color="gui.green"/>
|
||||
<Replace tag="[amount]" value="10" color="gui.green"/>
|
||||
<Replace tag="[skill]" value="stattypenames.mechanicalskillbonus" color="gui.orange"/>
|
||||
<Replace tag="[xpamount]" value="500" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.machinemaniac.50">
|
||||
<Replace tag="[requirement]" value="20" color="gui.green"/>
|
||||
<Replace tag="[level]" value="1" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.machinemaniac.100">
|
||||
<Replace tag="[requirement]" value="40" color="gui.green"/>
|
||||
<Replace tag="[amount]" value="50" color="gui.green"/>
|
||||
</Description>
|
||||
|
||||
<!-- Give the player stats that tracks if the rewards should be given -->
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityGivePermanentStat statidentifier="machinemaniac_30" value="1" maxvalue="1" setvalue="true" />
|
||||
<CharacterAbilityGivePermanentStat statidentifier="machinemaniac_50" value="1" maxvalue="1" setvalue="true" />
|
||||
<CharacterAbilityGivePermanentStat statidentifier="machinemaniac_100" value="1" maxvalue="1" setvalue="true" />
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
|
||||
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
|
||||
<Conditions>
|
||||
<AbilityConditionItem tags="fabricator,door,engine,oxygengenerator,pump,turretammosource,deconstructor,medicalfabricator,ductblock"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityGivePermanentStat statidentifier="machinemaniac_counter" value="1" removeondeath="false" />
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
|
||||
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
|
||||
<Conditions>
|
||||
<AbilityConditionHasPermanentStat statidentifier="machinemaniac_30" min="1"/>
|
||||
<AbilityConditionHasPermanentStat statidentifier="machinemaniac_counter" min="12"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveExperience amount="2000"/>
|
||||
<CharacterAbilityGivePermanentStat stattype="MechanicalSkillBonus" statidentifier="machinemaniac" value="10" setvalue="true" removeondeath="false" />
|
||||
<CharacterAbilityResetPermanentStat statidentifier="machinemaniac_30" />
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
|
||||
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
|
||||
<Conditions>
|
||||
<AbilityConditionHasPermanentStat statidentifier="machinemaniac_50" min="1"/>
|
||||
<AbilityConditionHasPermanentStat statidentifier="machinemaniac_counter" min="20"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityUpgradeSubmarine upgradeprefab="increasemaxpumpflow" upgradecategory="pumps" level="1" />
|
||||
<CharacterAbilityResetPermanentStat statidentifier="machinemaniac_50" />
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
|
||||
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
|
||||
<Conditions>
|
||||
<AbilityConditionHasPermanentStat statidentifier="machinemaniac_100" min="1"/>
|
||||
<AbilityConditionHasPermanentStat statidentifier="machinemaniac_counter" min="40"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityGivePermanentStat stattype="MechanicalRepairSpeed" statidentifier="machinemaniac" value="0.5" setvalue="true" removeondeath="false" />
|
||||
<CharacterAbilityResetPermanentStat statidentifier="machinemaniac_100" />
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="tinkerer">
|
||||
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="4,1" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.increasemaxrepairmechanical">
|
||||
<Replace tag="[percentage]" value="40" color="gui.green"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveStat stattype="MaxRepairConditionMultiplierMechanical" value="0.4"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="modularrepairs">
|
||||
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="5,1" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.unlockrecipe">
|
||||
<Replace tag="[itemname]" value="entityname.repairpack" color="gui.orange"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.freeupgrade">
|
||||
<Replace tag="[level]" value="1" color="gui.green"/>
|
||||
<Replace tag="[upgradename]" value="upgradename.decreaselowskillfixduration" color="gui.orange"/>
|
||||
</Description>
|
||||
<AddedRecipe itemidentifier="repairpack"/>
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityUpgradeSubmarine upgradeprefab="decreaselowskillfixduration" upgradecategory="electricaldevices" level="1" />
|
||||
<CharacterAbilityUpgradeSubmarine upgradeprefab="decreaselowskillfixduration" upgradecategory="mechanicaldevices" level="1" />
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="hullfixer">
|
||||
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="0,2" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.unlockrecipe">
|
||||
<Replace tag="[itemname]" value="entityname.fixfoamgrenade,entityname.handheldstatusmonitor" color="gui.orange"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.additionalstattype">
|
||||
<Replace tag="[amount]" value="25" color="gui.green"/>
|
||||
<Replace tag="[stattype]" value="stattypenames.repairtoolstructurerepairmultiplier" color="gui.orange"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveStat stattype="RepairToolStructureRepairMultiplier" value="0.25"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<AddedRecipe itemidentifier="fixfoamgrenade"/>
|
||||
<AddedRecipe itemidentifier="handheldstatusmonitor"/>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="letitdrain">
|
||||
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="1,2" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.letitdrain"/>
|
||||
<Description tag="talentdescription.letitdrainreminder">
|
||||
<Replace tag="[itemcount]" value="2" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.unlockrecipe">
|
||||
<Replace tag="[itemname]" value="entityname.portablepump" color="gui.orange"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityGivePermanentStat statidentifier="portablepump" stattype="MaxAttachableCount" value="2" />
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<AddedRecipe itemidentifier="portablepump"/>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="quickfixer">
|
||||
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="5,2" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.quickfixer">
|
||||
<Replace tag="[amount]" value="20" color="gui.green"/>
|
||||
<Replace tag="[duration]" value="10" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.repairmechanicaldevicestwiceasfast"/>
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveStat stattype="MechanicalRepairSpeed" value="1"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
|
||||
<Conditions>
|
||||
<AbilityConditionItem tags="fabricator,door,engine,oxygengenerator,pump,turretammosource,deconstructor,medicalfabricator,ductblock"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityApplyStatusEffects>
|
||||
<StatusEffects>
|
||||
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true">
|
||||
<Affliction identifier="quickfixer" amount="10.0"/>
|
||||
</StatusEffect>
|
||||
</StatusEffects>
|
||||
</CharacterAbilityApplyStatusEffects>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="scrapsavant">
|
||||
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="6,3" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.doublescrapoutput" />
|
||||
<Description tag="talentdescription.findadditionalscrap">
|
||||
<Replace tag="[probability]" value="20" color="gui.green"/>
|
||||
</Description>
|
||||
<AbilityGroupEffect abilityeffecttype="OnItemDeconstructedMaterial">
|
||||
<Conditions>
|
||||
<AbilityConditionItem tags="scrap"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityModifyValue multiplyvalue="2"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<AbilityGroupEffect abilityeffecttype="OnOpenItemContainer">
|
||||
<Conditions>
|
||||
<AbilityConditionItemInSubmarine submarinetype="Wreck"/>
|
||||
<AbilityConditionItem tags="container"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilitySpawnItemsToContainer randomchance="0.2" oncepercontainer="true">
|
||||
<StatusEffects>
|
||||
<StatusEffect type="OnAbility" target="UseTarget" >
|
||||
<SpawnItem identifiers="scrap" spawnposition="ThisInventory" spawnifcantbecontained="false" />
|
||||
</StatusEffect>
|
||||
</StatusEffects>
|
||||
</CharacterAbilitySpawnItemsToContainer>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>
|
||||
|
||||
<Talent identifier="safetyfirst">
|
||||
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="4,2" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.unlockrecipe">
|
||||
<Replace tag="[itemname]" value="entityname.safetyharness" color="gui.orange"/>
|
||||
</Description>
|
||||
<AddedRecipe itemidentifier="safetyharness"/>
|
||||
</Talent>
|
||||
|
||||
</Talents>
|
28
cmd/log_format_test/main.go
Normal file
28
cmd/log_format_test/main.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize logger with DEBUG level
|
||||
logger.Init(logger.LevelDebug)
|
||||
|
||||
// Test different log levels
|
||||
logger.Info("This is an info message")
|
||||
logger.Debug("This is a debug message")
|
||||
logger.Warning("This is a warning message")
|
||||
logger.Error("This is an error message")
|
||||
logger.Trace("This is a trace message (not visible at DEBUG level)")
|
||||
|
||||
// Test with a goroutine
|
||||
logger.SafeGo(func() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
logger.Info("Message from goroutine")
|
||||
})
|
||||
|
||||
// Wait for goroutine to complete
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
10
glob_test.go
10
glob_test.go
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cook/utils"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -76,9 +77,14 @@ func TestGlobExpansion(t *testing.T) {
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
files, err := expandFilePatterns(tc.patterns)
|
||||
// Convert string patterns to map[string]struct{} for ExpandGLobs
|
||||
patternMap := make(map[string]struct{})
|
||||
for _, pattern := range tc.patterns {
|
||||
patternMap[pattern] = struct{}{}
|
||||
}
|
||||
files, err := utils.ExpandGLobs(patternMap)
|
||||
if err != nil {
|
||||
t.Fatalf("expandFilePatterns failed: %v", err)
|
||||
t.Fatalf("ExpandGLobs failed: %v", err)
|
||||
}
|
||||
|
||||
if len(files) != tc.expected {
|
||||
|
51
go.mod
51
go.mod
@@ -1,39 +1,32 @@
|
||||
module modify
|
||||
module cook
|
||||
|
||||
go 1.24.1
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
github.com/PaesslerAG/jsonpath v0.1.1
|
||||
github.com/antchfx/xmlquery v1.4.4
|
||||
git.site.quack-lab.dev/dave/cylogger v1.3.0
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/yuin/gopher-lua v1.1.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/gorm v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.5 // indirect
|
||||
github.com/cloudflare/circl v1.6.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/PaesslerAG/gval v1.0.0 // indirect
|
||||
github.com/antchfx/xpath v1.3.3 // indirect
|
||||
github.com/go-git/go-git/v5 v5.14.0
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/hexops/valast v1.5.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
mvdan.cc/gofumpt v0.4.0 // indirect
|
||||
)
|
||||
|
||||
require gorm.io/driver/sqlite v1.6.0
|
||||
|
184
go.sum
184
go.sum
@@ -1,177 +1,59 @@
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8=
|
||||
github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I=
|
||||
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
|
||||
github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
|
||||
github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
|
||||
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
|
||||
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
|
||||
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
|
||||
github.com/antchfx/xpath v1.3.3 h1:tmuPQa1Uye0Ym1Zn65vxPgfltWb/Lxu2jeqIGteJSRs=
|
||||
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
git.site.quack-lab.dev/dave/cylogger v1.3.0 h1:eTWPUD+ThVi8kGIsRcE0XDeoH3yFb5miFEODyKUdWJw=
|
||||
git.site.quack-lab.dev/dave/cylogger v1.3.0/go.mod h1:wctgZplMvroA4X6p8f4B/LaCKtiBcT1Pp+L14kcS8jk=
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
|
||||
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
|
||||
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/hexops/autogold v0.8.1 h1:wvyd/bAJ+Dy+DcE09BoLk6r4Fa5R5W+O+GUzmR985WM=
|
||||
github.com/hexops/autogold v0.8.1/go.mod h1:97HLDXyG23akzAoRYJh/2OBs3kd80eHyKPvZw0S5ZBY=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hexops/valast v1.5.0 h1:FBTuvVi0wjTngtXJRZXMbkN/Dn6DgsUsBwch2DUJU8Y=
|
||||
github.com/hexops/valast v1.5.0/go.mod h1:Jcy1pNH7LNraVaAZDLyv21hHg2WBv9Nf9FL6fGxU7o4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM=
|
||||
mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ=
|
||||
|
534
main.go
534
main.go
@@ -3,17 +3,17 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"cook/processor"
|
||||
"cook/utils"
|
||||
|
||||
"modify/processor"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||
)
|
||||
|
||||
type GlobalStats struct {
|
||||
@@ -21,269 +21,401 @@ type GlobalStats struct {
|
||||
TotalModifications int
|
||||
ProcessedFiles int
|
||||
FailedFiles int
|
||||
ModificationsPerCommand sync.Map
|
||||
}
|
||||
|
||||
var stats GlobalStats
|
||||
var logger *log.Logger
|
||||
|
||||
var (
|
||||
jsonFlag = flag.Bool("json", false, "Process JSON files")
|
||||
xmlFlag = flag.Bool("xml", false, "Process XML files")
|
||||
gitFlag = flag.Bool("git", false, "Use git to manage files")
|
||||
resetFlag = flag.Bool("reset", false, "Reset files to their original state")
|
||||
repo *git.Repository
|
||||
worktree *git.Worktree
|
||||
stats GlobalStats = GlobalStats{
|
||||
ModificationsPerCommand: sync.Map{},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(log.Lmicroseconds | log.Lshortfile)
|
||||
logger = log.New(os.Stdout, "", log.Lmicroseconds|log.Lshortfile)
|
||||
|
||||
stats = GlobalStats{}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// TODO: Implement some sort of git integration
|
||||
// Maybe use go-git
|
||||
// Specify a -git flag
|
||||
// If we are operating with git then:
|
||||
// Inmitialize a repo if one doesn't exist (try to open right?)
|
||||
// For each file matched by glob first figure out if it's already tracked
|
||||
// If not tracked then track it and commit (either it alone or maybe multiple together somehow)
|
||||
// Then reset the file (to undo previous modifications)
|
||||
// THEN change the file
|
||||
// In addition add a -undo flag that will ONLY reset the files without changing them
|
||||
// Only for the ones matched by glob
|
||||
// ^ important because binary files would fuck us up
|
||||
flag.Usage = func() {
|
||||
CreateExampleConfig()
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [options] <pattern> <lua_expression> <...files_or_globs>\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "\nOptions:\n")
|
||||
fmt.Fprintf(os.Stderr, " -json\n")
|
||||
fmt.Fprintf(os.Stderr, " Process JSON files\n")
|
||||
fmt.Fprintf(os.Stderr, " -xml\n")
|
||||
fmt.Fprintf(os.Stderr, " Process XML files\n")
|
||||
fmt.Fprintf(os.Stderr, " -git\n")
|
||||
fmt.Fprintf(os.Stderr, " Use git to manage files\n")
|
||||
fmt.Fprintf(os.Stderr, " -reset\n")
|
||||
fmt.Fprintf(os.Stderr, " Reset files to their original state\n")
|
||||
fmt.Fprintf(os.Stderr, " -mode string\n")
|
||||
fmt.Fprintf(os.Stderr, " Processing mode: regex, xml, json (default \"regex\")\n")
|
||||
fmt.Fprintf(os.Stderr, " -loglevel string\n")
|
||||
fmt.Fprintf(os.Stderr, " Set logging level: ERROR, WARNING, INFO, DEBUG, TRACE (default \"INFO\")\n")
|
||||
fmt.Fprintf(os.Stderr, "\nExamples:\n")
|
||||
fmt.Fprintf(os.Stderr, " Regex mode (default):\n")
|
||||
fmt.Fprintf(os.Stderr, " %s \"<value>(\\d+)</value>\" \"*1.5\" data.xml\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, " XML mode:\n")
|
||||
fmt.Fprintf(os.Stderr, " %s -xml \"//value\" \"*1.5\" data.xml\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, " JSON mode:\n")
|
||||
fmt.Fprintf(os.Stderr, " %s -json \"$.items[*].value\" \"*1.5\" data.json\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, "\nNote: v1, v2, etc. are used to refer to capture groups as numbers.\n")
|
||||
fmt.Fprintf(os.Stderr, " s1, s2, etc. are used to refer to capture groups as strings.\n")
|
||||
fmt.Fprintf(os.Stderr, " Helper functions: num(str) converts string to number, str(num) converts number to string\n")
|
||||
fmt.Fprintf(os.Stderr, " is_number(str) checks if a string is numeric\n")
|
||||
fmt.Fprintf(os.Stderr, " For XML and JSON, the captured values are exposed as 'v', which can be of any type we capture (string, number, table).\n")
|
||||
fmt.Fprintf(os.Stderr, " If expression starts with an operator like *, /, +, -, =, etc., v1 is automatically prepended\n")
|
||||
fmt.Fprintf(os.Stderr, " You can use any valid Lua code, including if statements, loops, etc.\n")
|
||||
fmt.Fprintf(os.Stderr, " Glob patterns are supported for file selection (*.xml, data/**.xml, etc.)\n")
|
||||
}
|
||||
|
||||
// TODO: Fix bed shitting when doing *.yml in barotrauma directory
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
if *resetFlag {
|
||||
*gitFlag = true
|
||||
}
|
||||
|
||||
if len(args) < 3 {
|
||||
log.Printf("At least %d arguments are required", 3)
|
||||
logger.InitFlag()
|
||||
logger.Info("Initializing with log level: %s", logger.GetLevel().String())
|
||||
|
||||
if flag.NArg() == 0 {
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
// Get the appropriate pattern and expression based on mode
|
||||
var pattern, luaExpr string
|
||||
var filePatterns []string
|
||||
|
||||
pattern = args[0]
|
||||
luaExpr = args[1]
|
||||
filePatterns = args[2:]
|
||||
|
||||
// Prepare the Lua expression
|
||||
originalLuaExpr := luaExpr
|
||||
luaExpr = processor.BuildLuaScript(luaExpr)
|
||||
if originalLuaExpr != luaExpr {
|
||||
logger.Printf("Transformed Lua expression from %q to %q", originalLuaExpr, luaExpr)
|
||||
}
|
||||
|
||||
if *gitFlag {
|
||||
err := setupGit()
|
||||
db, err := utils.GetDB()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error setting up git: %v\n", err)
|
||||
logger.Error("Failed to get database: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Expand file patterns with glob support
|
||||
files, err := expandFilePatterns(filePatterns)
|
||||
workdone, err := HandleSpecialArgs(args, err, db)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error expanding file patterns: %v\n", err)
|
||||
logger.Error("Failed to handle special args: %v", err)
|
||||
return
|
||||
}
|
||||
if workdone {
|
||||
return
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "No files found matching the specified patterns\n")
|
||||
// The plan is:
|
||||
// Load all commands
|
||||
commands, err := utils.LoadCommands(args)
|
||||
if err != nil || len(commands) == 0 {
|
||||
logger.Error("Failed to load commands: %v", err)
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
if *gitFlag {
|
||||
err := cleanupGitFiles(files)
|
||||
if *utils.Filter != "" {
|
||||
logger.Info("Filtering commands by name: %s", *utils.Filter)
|
||||
commands = utils.FilterCommands(commands, *utils.Filter)
|
||||
logger.Info("Filtered %d commands", len(commands))
|
||||
}
|
||||
|
||||
// Then aggregate all the globs and deduplicate them
|
||||
globs := utils.AggregateGlobs(commands)
|
||||
logger.Debug("Aggregated %d globs before deduplication", utils.CountGlobsBeforeDedup(commands))
|
||||
|
||||
for _, command := range commands {
|
||||
logger.Trace("Command: %s", command.Name)
|
||||
logger.Trace("Regex: %s", command.Regex)
|
||||
logger.Trace("Files: %v", command.Files)
|
||||
logger.Trace("Lua: %s", command.Lua)
|
||||
logger.Trace("Reset: %t", command.Reset)
|
||||
logger.Trace("Isolate: %t", command.Isolate)
|
||||
logger.Trace("LogLevel: %s", command.LogLevel)
|
||||
}
|
||||
|
||||
// Resolve all the files for all the globs
|
||||
logger.Info("Found %d unique file patterns", len(globs))
|
||||
files, err := utils.ExpandGLobs(globs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error cleaning up git files: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if *resetFlag {
|
||||
log.Printf("Files reset to their original state, nothing more to do")
|
||||
logger.Error("Failed to expand file patterns: %v", err)
|
||||
return
|
||||
}
|
||||
logger.Info("Found %d files to process", len(files))
|
||||
|
||||
// Create the processor based on mode
|
||||
var proc processor.Processor
|
||||
switch {
|
||||
case *xmlFlag:
|
||||
proc = &processor.XMLProcessor{}
|
||||
logger.Printf("Starting XML modifier with XPath %q, expression %q on %d files",
|
||||
pattern, luaExpr, len(files))
|
||||
case *jsonFlag:
|
||||
proc = &processor.JSONProcessor{}
|
||||
logger.Printf("Starting JSON modifier with JSONPath %q, expression %q on %d files",
|
||||
pattern, luaExpr, len(files))
|
||||
default:
|
||||
proc = &processor.RegexProcessor{}
|
||||
logger.Printf("Starting regex modifier with pattern %q, expression %q on %d files",
|
||||
pattern, luaExpr, len(files))
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
// Process each file
|
||||
for _, file := range files {
|
||||
wg.Add(1)
|
||||
go func(file string) {
|
||||
defer wg.Done()
|
||||
logger.Printf("Processing file: %s", file)
|
||||
|
||||
// It's a bit fucked, maybe I could do better to call it from proc... But it'll do for now
|
||||
modCount, matchCount, err := processor.Process(proc, file, pattern, luaExpr)
|
||||
// Somehow connect files to commands via globs..
|
||||
// For each file check every glob of every command
|
||||
// Maybe memoize this part
|
||||
// That way we know what commands affect what files
|
||||
associations, err := utils.AssociateFilesWithCommands(files, commands)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", file, err)
|
||||
stats.FailedFiles++
|
||||
logger.Error("Failed to associate files with commands: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = utils.ResetWhereNecessary(associations, db)
|
||||
if err != nil {
|
||||
logger.Error("Failed to reset files where necessary: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Then for each file run all commands associated with the file
|
||||
workers := make(chan struct{}, *utils.ParallelFiles)
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
// Add performance tracking
|
||||
startTime := time.Now()
|
||||
var fileMutex sync.Mutex
|
||||
|
||||
// Create a map to store loggers for each command
|
||||
commandLoggers := make(map[string]*logger.Logger)
|
||||
for _, command := range commands {
|
||||
// Create a named logger for each command
|
||||
cmdName := command.Name
|
||||
if cmdName == "" {
|
||||
// If no name is provided, use a short version of the regex pattern
|
||||
if len(command.Regex) > 20 {
|
||||
cmdName = command.Regex[:17] + "..."
|
||||
} else {
|
||||
logger.Printf("Successfully processed file: %s", file)
|
||||
stats.ProcessedFiles++
|
||||
stats.TotalMatches += matchCount
|
||||
stats.TotalModifications += modCount
|
||||
cmdName = command.Regex
|
||||
}
|
||||
}(file)
|
||||
}
|
||||
|
||||
// Parse the log level for this specific command
|
||||
cmdLogLevel := logger.ParseLevel(command.LogLevel)
|
||||
|
||||
// Create a logger with the command name as a field
|
||||
commandLoggers[command.Name] = logger.WithField("command", cmdName)
|
||||
commandLoggers[command.Name].SetLevel(cmdLogLevel)
|
||||
|
||||
logger.Debug("Created logger for command %q with log level %s", cmdName, cmdLogLevel.String())
|
||||
}
|
||||
|
||||
for file, association := range associations {
|
||||
workers <- struct{}{}
|
||||
wg.Add(1)
|
||||
logger.SafeGoWithArgs(func(args ...interface{}) {
|
||||
defer func() { <-workers }()
|
||||
defer wg.Done()
|
||||
// Track per-file processing time
|
||||
fileStartTime := time.Now()
|
||||
|
||||
logger.Debug("Reading file %q", file)
|
||||
fileData, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
logger.Error("Failed to read file %q: %v", file, err)
|
||||
return
|
||||
}
|
||||
fileDataStr := string(fileData)
|
||||
|
||||
logger.Debug("Saving file %q to database", file)
|
||||
err = db.SaveFile(file, fileData)
|
||||
if err != nil {
|
||||
logger.Error("Failed to save file %q to database: %v", file, err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("Running isolate commands for file %q", file)
|
||||
fileDataStr, err = RunIsolateCommands(association, file, fileDataStr, &fileMutex)
|
||||
if err != nil {
|
||||
logger.Error("Failed to run isolate commands for file %q: %v", file, err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("Running other commands for file %q", file)
|
||||
fileDataStr, err = RunOtherCommands(file, fileDataStr, association, &fileMutex, commandLoggers)
|
||||
if err != nil {
|
||||
logger.Error("Failed to run other commands for file %q: %v", file, err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("Writing file %q", file)
|
||||
err = os.WriteFile(file, []byte(fileDataStr), 0644)
|
||||
if err != nil {
|
||||
logger.Error("Failed to write file %q: %v", file, err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("File %q processed in %v", file, time.Since(fileStartTime))
|
||||
}, file, commands)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
processingTime := time.Since(startTime)
|
||||
logger.Info("Processing completed in %v", processingTime)
|
||||
if stats.ProcessedFiles > 0 {
|
||||
logger.Info("Average time per file: %v", processingTime/time.Duration(stats.ProcessedFiles))
|
||||
}
|
||||
|
||||
// TODO: Also give each command its own logger, maybe prefix it with something... Maybe give commands a name?
|
||||
// Do that with logger.WithField("loglevel", level.String())
|
||||
// Since each command also has its own log level
|
||||
// TODO: Maybe even figure out how to run individual commands...?
|
||||
// TODO: What to do with git? Figure it out ....
|
||||
|
||||
// if *gitFlag {
|
||||
// logger.Info("Git integration enabled, setting up git repository")
|
||||
// err := setupGit()
|
||||
// if err != nil {
|
||||
// logger.Error("Failed to setup git: %v", err)
|
||||
// fmt.Fprintf(os.Stderr, "Error setting up git: %v\n", err)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
// logger.Debug("Expanding file patterns")
|
||||
// files, err := expandFilePatterns(filePatterns)
|
||||
// if err != nil {
|
||||
// logger.Error("Failed to expand file patterns: %v", err)
|
||||
// fmt.Fprintf(os.Stderr, "Error expanding file patterns: %v\n", err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// if *gitFlag {
|
||||
// logger.Info("Cleaning up git files before processing")
|
||||
// err := cleanupGitFiles(files)
|
||||
// if err != nil {
|
||||
// logger.Error("Failed to cleanup git files: %v", err)
|
||||
// fmt.Fprintf(os.Stderr, "Error cleaning up git files: %v\n", err)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// if *resetFlag {
|
||||
// logger.Info("Files reset to their original state, nothing more to do")
|
||||
// log.Printf("Files reset to their original state, nothing more to do")
|
||||
// return
|
||||
// }
|
||||
|
||||
// Print summary
|
||||
if stats.TotalModifications == 0 {
|
||||
fmt.Fprintf(os.Stderr, "No modifications were made in any files\n")
|
||||
logger.Warning("No modifications were made in any files")
|
||||
} else {
|
||||
fmt.Printf("Operation complete! Modified %d values in %d/%d files\n",
|
||||
logger.Info("Operation complete! Modified %d values in %d/%d files",
|
||||
stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles)
|
||||
}
|
||||
}
|
||||
sortedCommands := []string{}
|
||||
stats.ModificationsPerCommand.Range(func(key, value interface{}) bool {
|
||||
sortedCommands = append(sortedCommands, key.(string))
|
||||
return true
|
||||
})
|
||||
sort.Strings(sortedCommands)
|
||||
|
||||
func setupGit() error {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current working directory: %w", err)
|
||||
}
|
||||
logger.Printf("Current working directory obtained: %s", cwd)
|
||||
|
||||
logger.Printf("Attempting to open git repository at %s", cwd)
|
||||
repo, err = git.PlainOpen(cwd)
|
||||
if err != nil {
|
||||
logger.Printf("No existing git repository found at %s, attempting to initialize a new git repository.", cwd)
|
||||
repo, err = git.PlainInit(cwd, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize a new git repository at %s: %w", cwd, err)
|
||||
}
|
||||
logger.Printf("Successfully initialized a new git repository at %s", cwd)
|
||||
for _, command := range sortedCommands {
|
||||
count, _ := stats.ModificationsPerCommand.Load(command)
|
||||
if count.(int) > 0 {
|
||||
logger.Info("\tCommand %q made %d modifications", command, count)
|
||||
} else {
|
||||
logger.Printf("Successfully opened existing git repository at %s", cwd)
|
||||
logger.Warning("\tCommand %q made no modifications", command)
|
||||
}
|
||||
}
|
||||
|
||||
logger.Printf("Attempting to obtain worktree for repository at %s", cwd)
|
||||
worktree, err = repo.Worktree()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to obtain worktree for repository at %s: %w", cwd, err)
|
||||
}
|
||||
logger.Printf("Successfully obtained worktree for repository at %s", cwd)
|
||||
return nil
|
||||
}
|
||||
|
||||
func expandFilePatterns(patterns []string) ([]string, error) {
|
||||
var files []string
|
||||
filesMap := make(map[string]bool)
|
||||
|
||||
for _, pattern := range patterns {
|
||||
matches, _ := doublestar.Glob(os.DirFS("."), pattern)
|
||||
for _, m := range matches {
|
||||
if info, err := os.Stat(m); err == nil && !info.IsDir() && !filesMap[m] {
|
||||
filesMap[m], files = true, append(files, m)
|
||||
func HandleSpecialArgs(args []string, err error, db utils.DB) (bool, error) {
|
||||
switch args[0] {
|
||||
case "reset":
|
||||
err = utils.ResetAllFiles(db)
|
||||
if err != nil {
|
||||
logger.Error("Failed to reset all files: %v", err)
|
||||
return true, err
|
||||
}
|
||||
logger.Info("All files reset")
|
||||
return true, nil
|
||||
case "dump":
|
||||
err = db.RemoveAllFiles()
|
||||
if err != nil {
|
||||
logger.Error("Failed to remove all files from database: %v", err)
|
||||
return true, err
|
||||
}
|
||||
logger.Info("All files removed from database")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if len(files) > 0 {
|
||||
logger.Printf("Found %d files to process", len(files))
|
||||
}
|
||||
return files, nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func cleanupGitFiles(files []string) error {
|
||||
for _, file := range files {
|
||||
logger.Printf("Checking file: %s", file)
|
||||
status, err := worktree.Status()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error getting worktree status: %v\n", err)
|
||||
return fmt.Errorf("error getting worktree status: %w", err)
|
||||
}
|
||||
if status.IsUntracked(file) {
|
||||
logger.Printf("Detected untracked file: %s. Attempting to add it to the git index.", file)
|
||||
_, err = worktree.Add(file)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error adding file to git: %v\n", err)
|
||||
return fmt.Errorf("error adding file to git: %w", err)
|
||||
}
|
||||
|
||||
filename := filepath.Base(file)
|
||||
logger.Printf("File %s added successfully. Now committing it with message: 'Track %s'", filename, filename)
|
||||
_, err = worktree.Commit("Track "+filename, &git.CommitOptions{
|
||||
Author: &object.Signature{
|
||||
Name: "Big Chef",
|
||||
Email: "bigchef@bigchef.com",
|
||||
When: time.Now(),
|
||||
func CreateExampleConfig() {
|
||||
commands := []utils.ModifyCommand{
|
||||
{
|
||||
Name: "DoubleNumericValues",
|
||||
Regex: "<value>(\\d+)</value>",
|
||||
Lua: "v1 * 2",
|
||||
Files: []string{"data/*.xml"},
|
||||
LogLevel: "INFO",
|
||||
},
|
||||
})
|
||||
{
|
||||
Name: "UpdatePrices",
|
||||
Regex: "price=\"(\\d+)\"",
|
||||
Lua: "if num(v1) < 100 then return v1 * 1.5 else return v1 end",
|
||||
Files: []string{"items/*.xml", "shop/*.xml"},
|
||||
LogLevel: "DEBUG",
|
||||
},
|
||||
{
|
||||
Name: "IsolatedTagUpdate",
|
||||
Regex: "<tag>(.*?)</tag>",
|
||||
Lua: "string.upper(s1)",
|
||||
Files: []string{"config.xml"},
|
||||
Isolate: true,
|
||||
NoDedup: true,
|
||||
LogLevel: "TRACE",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(commands)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error committing file: %v\n", err)
|
||||
return fmt.Errorf("error committing file: %w", err)
|
||||
logger.Error("Failed to marshal example config: %v", err)
|
||||
return
|
||||
}
|
||||
logger.Printf("Successfully committed file: %s with message: 'Track %s'", filename, filename)
|
||||
} else {
|
||||
logger.Printf("File %s is already tracked. Restoring it to the working tree.", file)
|
||||
err := worktree.Restore(&git.RestoreOptions{
|
||||
Files: []string{file},
|
||||
Staged: true,
|
||||
Worktree: true,
|
||||
})
|
||||
|
||||
err = os.WriteFile("example_cook.yml", data, 0644)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error restoring file: %v\n", err)
|
||||
return fmt.Errorf("error restoring file: %w", err)
|
||||
logger.Error("Failed to write example_cook.yml: %v", err)
|
||||
return
|
||||
}
|
||||
logger.Printf("File %s restored successfully.", file)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
logger.Info("Wrote example_cook.yml")
|
||||
}
|
||||
|
||||
func RunOtherCommands(file string, fileDataStr string, association utils.FileCommandAssociation, fileMutex *sync.Mutex, commandLoggers map[string]*logger.Logger) (string, error) {
|
||||
// Aggregate all the modifications and execute them
|
||||
modifications := []utils.ReplaceCommand{}
|
||||
for _, command := range association.Commands {
|
||||
// Use command-specific logger if available, otherwise fall back to default logger
|
||||
cmdLogger := logger.Default
|
||||
if cmdLog, ok := commandLoggers[command.Name]; ok {
|
||||
cmdLogger = cmdLog
|
||||
}
|
||||
|
||||
cmdLogger.Info("Processing file %q with command %q", file, command.Regex)
|
||||
newModifications, err := processor.ProcessRegex(fileDataStr, command, file)
|
||||
if err != nil {
|
||||
logger.Error("Failed to process file %q with command %q: %v", file, command.Regex, err)
|
||||
continue
|
||||
}
|
||||
modifications = append(modifications, newModifications...)
|
||||
// It is not guranteed that all the commands will be executed...
|
||||
// TODO: Make this better
|
||||
// We'd have to pass the map to executemodifications or something...
|
||||
count, ok := stats.ModificationsPerCommand.Load(command.Name)
|
||||
if !ok {
|
||||
count = 0
|
||||
}
|
||||
stats.ModificationsPerCommand.Store(command.Name, count.(int)+len(newModifications))
|
||||
|
||||
cmdLogger.Debug("Command %q generated %d modifications", command.Name, len(newModifications))
|
||||
}
|
||||
|
||||
if len(modifications) == 0 {
|
||||
logger.Warning("No modifications found for file %q", file)
|
||||
return fileDataStr, nil
|
||||
}
|
||||
|
||||
// Sort commands in reverse order for safe replacements
|
||||
var count int
|
||||
fileDataStr, count = utils.ExecuteModifications(modifications, fileDataStr)
|
||||
|
||||
fileMutex.Lock()
|
||||
stats.ProcessedFiles++
|
||||
stats.TotalModifications += count
|
||||
fileMutex.Unlock()
|
||||
|
||||
logger.Info("Executed %d modifications for file %q", count, file)
|
||||
return fileDataStr, nil
|
||||
}
|
||||
|
||||
func RunIsolateCommands(association utils.FileCommandAssociation, file string, fileDataStr string, fileMutex *sync.Mutex) (string, error) {
|
||||
for _, isolateCommand := range association.IsolateCommands {
|
||||
logger.Info("Processing file %q with isolate command %q", file, isolateCommand.Regex)
|
||||
modifications, err := processor.ProcessRegex(fileDataStr, isolateCommand, file)
|
||||
if err != nil {
|
||||
logger.Error("Failed to process file %q with isolate command %q: %v", file, isolateCommand.Regex, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(modifications) == 0 {
|
||||
logger.Warning("No modifications found for file %q", file)
|
||||
continue
|
||||
}
|
||||
|
||||
var count int
|
||||
fileDataStr, count = utils.ExecuteModifications(modifications, fileDataStr)
|
||||
|
||||
fileMutex.Lock()
|
||||
stats.ProcessedFiles++
|
||||
stats.TotalModifications += count
|
||||
fileMutex.Unlock()
|
||||
|
||||
logger.Info("Executed %d isolate modifications for file %q", count, file)
|
||||
}
|
||||
return fileDataStr, nil
|
||||
}
|
||||
|
@@ -1,165 +0,0 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"modify/processor/jsonpath"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// JSONProcessor implements the Processor interface for JSON documents
|
||||
type JSONProcessor struct{}
|
||||
|
||||
// ProcessContent implements the Processor interface for JSONProcessor
|
||||
func (p *JSONProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
|
||||
// Parse JSON document
|
||||
var jsonData interface{}
|
||||
err := json.Unmarshal([]byte(content), &jsonData)
|
||||
if err != nil {
|
||||
return content, 0, 0, fmt.Errorf("error parsing JSON: %v", err)
|
||||
}
|
||||
|
||||
// Find nodes matching the JSONPath pattern
|
||||
nodes, err := jsonpath.Get(jsonData, pattern)
|
||||
if err != nil {
|
||||
return content, 0, 0, fmt.Errorf("error getting nodes: %v", err)
|
||||
}
|
||||
|
||||
matchCount := len(nodes)
|
||||
if matchCount == 0 {
|
||||
return content, 0, 0, nil
|
||||
}
|
||||
|
||||
modCount := 0
|
||||
for _, node := range nodes {
|
||||
log.Printf("Processing node at path: %s with value: %v", node.Path, node.Value)
|
||||
|
||||
// Initialize Lua
|
||||
L, err := NewLuaState()
|
||||
if err != nil {
|
||||
return content, len(nodes), 0, fmt.Errorf("error creating Lua state: %v", err)
|
||||
}
|
||||
defer L.Close()
|
||||
log.Println("Lua state initialized successfully.")
|
||||
|
||||
err = p.ToLua(L, node.Value)
|
||||
if err != nil {
|
||||
return content, len(nodes), 0, fmt.Errorf("error converting to Lua: %v", err)
|
||||
}
|
||||
log.Printf("Converted node value to Lua: %v", node.Value)
|
||||
|
||||
originalScript := luaExpr
|
||||
fullScript := BuildLuaScript(luaExpr)
|
||||
log.Printf("Original script: %q, Full script: %q", originalScript, fullScript)
|
||||
|
||||
// Execute Lua script
|
||||
log.Printf("Executing Lua script: %q", fullScript)
|
||||
if err := L.DoString(fullScript); err != nil {
|
||||
return content, len(nodes), 0, fmt.Errorf("error executing Lua %q: %v", fullScript, err)
|
||||
}
|
||||
log.Println("Lua script executed successfully.")
|
||||
|
||||
// Get modified value
|
||||
result, err := p.FromLua(L)
|
||||
if err != nil {
|
||||
return content, len(nodes), 0, fmt.Errorf("error getting result from Lua: %v", err)
|
||||
}
|
||||
log.Printf("Retrieved modified value from Lua: %v", result)
|
||||
|
||||
modified := false
|
||||
modified = L.GetGlobal("modified").String() == "true"
|
||||
if !modified {
|
||||
log.Printf("No changes made to node at path: %s", node.Path)
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply the modification to the JSON data
|
||||
err = p.updateJSONValue(jsonData, node.Path, result)
|
||||
if err != nil {
|
||||
return content, len(nodes), 0, fmt.Errorf("error updating JSON: %v", err)
|
||||
}
|
||||
log.Printf("Updated JSON at path: %s with new value: %v", node.Path, result)
|
||||
modCount++
|
||||
}
|
||||
|
||||
// Convert the modified JSON back to a string with same formatting
|
||||
var jsonBytes []byte
|
||||
jsonBytes, err = json.MarshalIndent(jsonData, "", " ")
|
||||
if err != nil {
|
||||
return content, modCount, matchCount, fmt.Errorf("error marshalling JSON: %v", err)
|
||||
}
|
||||
return string(jsonBytes), modCount, matchCount, nil
|
||||
}
|
||||
|
||||
// updateJSONValue updates a value in the JSON structure based on its JSONPath
|
||||
func (p *JSONProcessor) updateJSONValue(jsonData interface{}, path string, newValue interface{}) error {
|
||||
// Special handling for root node
|
||||
if path == "$" {
|
||||
// For the root node, we'll copy the value to the jsonData reference
|
||||
// This is a special case since we can't directly replace the interface{} variable
|
||||
|
||||
// We need to handle different types of root elements
|
||||
switch rootValue := newValue.(type) {
|
||||
case map[string]interface{}:
|
||||
// For objects, we need to copy over all keys
|
||||
rootMap, ok := jsonData.(map[string]interface{})
|
||||
if !ok {
|
||||
// If the original wasn't a map, completely replace it with the new map
|
||||
// This is handled by the jsonpath.Set function
|
||||
return jsonpath.Set(jsonData, path, newValue)
|
||||
}
|
||||
|
||||
// Clear the original map
|
||||
for k := range rootMap {
|
||||
delete(rootMap, k)
|
||||
}
|
||||
|
||||
// Copy all keys from the new map
|
||||
for k, v := range rootValue {
|
||||
rootMap[k] = v
|
||||
}
|
||||
return nil
|
||||
|
||||
case []interface{}:
|
||||
// For arrays, we need to handle similarly
|
||||
rootArray, ok := jsonData.([]interface{})
|
||||
if !ok {
|
||||
// If the original wasn't an array, use jsonpath.Set
|
||||
return jsonpath.Set(jsonData, path, newValue)
|
||||
}
|
||||
|
||||
// Clear and recreate the array
|
||||
*&rootArray = rootValue
|
||||
return nil
|
||||
|
||||
default:
|
||||
// For other types, use jsonpath.Set
|
||||
return jsonpath.Set(jsonData, path, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
// For non-root paths, use the regular Set method
|
||||
err := jsonpath.Set(jsonData, path, newValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update JSON value at path '%s': %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToLua converts JSON values to Lua variables
|
||||
func (p *JSONProcessor) ToLua(L *lua.LState, data interface{}) error {
|
||||
table, err := ToLua(L, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
L.SetGlobal("v", table)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FromLua retrieves values from Lua
|
||||
func (p *JSONProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
||||
luaValue := L.GetGlobal("v")
|
||||
return FromLua(L, luaValue)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,490 +0,0 @@
|
||||
package jsonpath
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// JSONStep represents a single step in a JSONPath query
|
||||
type JSONStep struct {
|
||||
Type StepType
|
||||
Key string // For Child/RecursiveDescent
|
||||
Index int // For Index (use -1 for wildcard "*")
|
||||
}
|
||||
|
||||
// JSONNode represents a value in the JSON data with its path
|
||||
type JSONNode struct {
|
||||
Value interface{} // The value found at the path
|
||||
Path string // The exact JSONPath where the value was found
|
||||
}
|
||||
|
||||
// StepType defines the types of steps in a JSONPath
|
||||
type StepType int
|
||||
|
||||
const (
|
||||
RootStep StepType = iota // $ - The root element
|
||||
ChildStep // .key - Direct child access
|
||||
RecursiveDescentStep // ..key - Recursive search for key
|
||||
WildcardStep // .* - All children of an object
|
||||
IndexStep // [n] - Array index access (or [*] for all elements)
|
||||
)
|
||||
|
||||
// TraversalMode determines how the traversal behaves
|
||||
type TraversalMode int
|
||||
|
||||
const (
|
||||
CollectMode TraversalMode = iota // Just collect matched nodes
|
||||
ModifyFirstMode // Modify first matching node
|
||||
ModifyAllMode // Modify all matching nodes
|
||||
)
|
||||
|
||||
// ParseJSONPath parses a JSONPath string into a sequence of steps
|
||||
func ParseJSONPath(path string) ([]JSONStep, error) {
|
||||
if len(path) == 0 || path[0] != '$' {
|
||||
return nil, fmt.Errorf("path must start with $; received: %q", path)
|
||||
}
|
||||
|
||||
steps := []JSONStep{}
|
||||
i := 0
|
||||
|
||||
for i < len(path) {
|
||||
switch path[i] {
|
||||
case '$':
|
||||
steps = append(steps, JSONStep{Type: RootStep})
|
||||
i++
|
||||
case '.':
|
||||
i++
|
||||
if i < len(path) && path[i] == '.' {
|
||||
// Recursive descent
|
||||
i++
|
||||
key, nextPos := readKey(path, i)
|
||||
steps = append(steps, JSONStep{Type: RecursiveDescentStep, Key: key})
|
||||
i = nextPos
|
||||
} else {
|
||||
// Child step or wildcard
|
||||
key, nextPos := readKey(path, i)
|
||||
if key == "*" {
|
||||
steps = append(steps, JSONStep{Type: WildcardStep})
|
||||
} else {
|
||||
steps = append(steps, JSONStep{Type: ChildStep, Key: key})
|
||||
}
|
||||
i = nextPos
|
||||
}
|
||||
case '[':
|
||||
// Index step
|
||||
i++
|
||||
indexStr, nextPos := readIndex(path, i)
|
||||
if indexStr == "*" {
|
||||
steps = append(steps, JSONStep{Type: IndexStep, Index: -1})
|
||||
} else {
|
||||
index, err := strconv.Atoi(indexStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid index: %s; error: %w", indexStr, err)
|
||||
}
|
||||
steps = append(steps, JSONStep{Type: IndexStep, Index: index})
|
||||
}
|
||||
i = nextPos + 1 // Skip closing ]
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected character: %c at position %d; path: %q", path[i], i, path)
|
||||
}
|
||||
}
|
||||
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
// readKey extracts a key name from the path
|
||||
func readKey(path string, start int) (string, int) {
|
||||
i := start
|
||||
for ; i < len(path); i++ {
|
||||
if path[i] == '.' || path[i] == '[' {
|
||||
break
|
||||
}
|
||||
}
|
||||
return path[start:i], i
|
||||
}
|
||||
|
||||
// readIndex extracts an array index or wildcard from the path
|
||||
func readIndex(path string, start int) (string, int) {
|
||||
i := start
|
||||
for ; i < len(path); i++ {
|
||||
if path[i] == ']' {
|
||||
break
|
||||
}
|
||||
}
|
||||
return path[start:i], i
|
||||
}
|
||||
|
||||
// Get retrieves values with their paths from data at the specified JSONPath
|
||||
// Each returned JSONNode contains both the value and its exact path in the data structure
|
||||
func Get(data interface{}, path string) ([]JSONNode, error) {
|
||||
steps, err := ParseJSONPath(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
||||
}
|
||||
|
||||
results := []JSONNode{}
|
||||
err = traverseWithPaths(data, steps, &results, "$")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to traverse JSONPath %q: %w", path, err)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// Set updates the value at the specified JSONPath in the original data structure.
|
||||
// It only modifies the first matching node.
|
||||
func Set(data interface{}, path string, value interface{}) error {
|
||||
steps, err := ParseJSONPath(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
||||
}
|
||||
|
||||
success := false
|
||||
err = setWithPath(data, steps, &success, value, "$", ModifyFirstMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAll updates all matching values at the specified JSONPath.
|
||||
func SetAll(data interface{}, path string, value interface{}) error {
|
||||
steps, err := ParseJSONPath(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse JSONPath %q: %w", path, err)
|
||||
}
|
||||
|
||||
success := false
|
||||
err = setWithPath(data, steps, &success, value, "$", ModifyAllMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setWithPath modifies values while tracking paths
|
||||
func setWithPath(node interface{}, steps []JSONStep, success *bool, value interface{}, currentPath string, mode TraversalMode) error {
|
||||
if node == nil || *success && mode == ModifyFirstMode {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip root step
|
||||
actualSteps := steps
|
||||
if len(steps) > 0 && steps[0].Type == RootStep {
|
||||
actualSteps = steps[1:]
|
||||
}
|
||||
|
||||
// If we have no steps left, we're setting the root value
|
||||
if len(actualSteps) == 0 {
|
||||
// For the root node, we need to handle it differently depending on what's passed in
|
||||
// since we can't directly replace the interface{} variable
|
||||
|
||||
// We'll signal success and let the JSONProcessor handle updating the root
|
||||
*success = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Process the first step
|
||||
step := actualSteps[0]
|
||||
remainingSteps := actualSteps[1:]
|
||||
isLastStep := len(remainingSteps) == 0
|
||||
|
||||
switch step.Type {
|
||||
case ChildStep:
|
||||
m, ok := node.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("node at path %q is not a map; actual type: %T", currentPath, node)
|
||||
}
|
||||
|
||||
childPath := currentPath + "." + step.Key
|
||||
|
||||
if isLastStep {
|
||||
// We've reached the target, set the value
|
||||
m[step.Key] = value
|
||||
*success = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create intermediate nodes if necessary
|
||||
child, exists := m[step.Key]
|
||||
if !exists {
|
||||
// Create missing intermediate node
|
||||
if len(remainingSteps) > 0 && remainingSteps[0].Type == IndexStep {
|
||||
child = []interface{}{}
|
||||
} else {
|
||||
child = map[string]interface{}{}
|
||||
}
|
||||
m[step.Key] = child
|
||||
}
|
||||
|
||||
err := setWithPath(child, remainingSteps, success, value, childPath, mode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", childPath, err)
|
||||
}
|
||||
|
||||
case IndexStep:
|
||||
arr, ok := node.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("node at path %q is not an array; actual type: %T", currentPath, node)
|
||||
}
|
||||
|
||||
// Handle wildcard index
|
||||
if step.Index == -1 {
|
||||
for i, item := range arr {
|
||||
itemPath := fmt.Sprintf("%s[%d]", currentPath, i)
|
||||
if isLastStep {
|
||||
arr[i] = value
|
||||
*success = true
|
||||
if mode == ModifyFirstMode {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
err := setWithPath(item, remainingSteps, success, value, itemPath, mode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", itemPath, err)
|
||||
}
|
||||
if *success && mode == ModifyFirstMode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle specific index
|
||||
if step.Index >= 0 && step.Index < len(arr) {
|
||||
item := arr[step.Index]
|
||||
itemPath := fmt.Sprintf("%s[%d]", currentPath, step.Index)
|
||||
if isLastStep {
|
||||
arr[step.Index] = value
|
||||
*success = true
|
||||
} else {
|
||||
err := setWithPath(item, remainingSteps, success, value, itemPath, mode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", itemPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case RecursiveDescentStep:
|
||||
// For recursive descent, first check direct match at this level
|
||||
if m, ok := node.(map[string]interface{}); ok && step.Key != "*" {
|
||||
if val, exists := m[step.Key]; exists {
|
||||
directPath := currentPath + "." + step.Key
|
||||
if isLastStep {
|
||||
m[step.Key] = value
|
||||
*success = true
|
||||
if mode == ModifyFirstMode {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
err := setWithPath(val, remainingSteps, success, value, directPath, mode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", directPath, err)
|
||||
}
|
||||
if *success && mode == ModifyFirstMode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then continue recursion to all children
|
||||
switch n := node.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, v := range n {
|
||||
childPath := currentPath + "." + k
|
||||
// Skip keys we've already processed directly
|
||||
if step.Key != "*" && k == step.Key {
|
||||
continue
|
||||
}
|
||||
err := setWithPath(v, steps, success, value, childPath, mode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", childPath, err)
|
||||
}
|
||||
if *success && mode == ModifyFirstMode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for i, v := range n {
|
||||
childPath := fmt.Sprintf("%s[%d]", currentPath, i)
|
||||
err := setWithPath(v, steps, success, value, childPath, mode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", childPath, err)
|
||||
}
|
||||
if *success && mode == ModifyFirstMode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case WildcardStep:
|
||||
m, ok := node.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("node at path %q is not a map; actual type: %T", currentPath, node)
|
||||
}
|
||||
|
||||
for k, v := range m {
|
||||
childPath := currentPath + "." + k
|
||||
if isLastStep {
|
||||
m[k] = value
|
||||
*success = true
|
||||
if mode == ModifyFirstMode {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
err := setWithPath(v, remainingSteps, success, value, childPath, mode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set value at JSONPath %q: %w", childPath, err)
|
||||
}
|
||||
if *success && mode == ModifyFirstMode {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// traverseWithPaths tracks both nodes and their paths during traversal
|
||||
func traverseWithPaths(node interface{}, steps []JSONStep, results *[]JSONNode, currentPath string) error {
|
||||
if len(steps) == 0 || node == nil {
|
||||
return fmt.Errorf("cannot traverse with empty steps or nil node; steps length: %d, node: %v", len(steps), node)
|
||||
}
|
||||
|
||||
// Skip root step
|
||||
actualSteps := steps
|
||||
if steps[0].Type == RootStep {
|
||||
if len(steps) == 1 {
|
||||
*results = append(*results, JSONNode{Value: node, Path: currentPath})
|
||||
return nil
|
||||
}
|
||||
actualSteps = steps[1:]
|
||||
}
|
||||
|
||||
// Process the first step
|
||||
step := actualSteps[0]
|
||||
remainingSteps := actualSteps[1:]
|
||||
isLastStep := len(remainingSteps) == 0
|
||||
|
||||
switch step.Type {
|
||||
case ChildStep:
|
||||
m, ok := node.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("node is not a map; actual type: %T", node)
|
||||
}
|
||||
|
||||
child, exists := m[step.Key]
|
||||
if !exists {
|
||||
return fmt.Errorf("key not found: %s in node at path: %s", step.Key, currentPath)
|
||||
}
|
||||
|
||||
childPath := currentPath + "." + step.Key
|
||||
if isLastStep {
|
||||
*results = append(*results, JSONNode{Value: child, Path: childPath})
|
||||
} else {
|
||||
err := traverseWithPaths(child, remainingSteps, results, childPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", childPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
case IndexStep:
|
||||
arr, ok := node.([]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("node is not an array; actual type: %T", node)
|
||||
}
|
||||
|
||||
// Handle wildcard index
|
||||
if step.Index == -1 {
|
||||
for i, item := range arr {
|
||||
itemPath := fmt.Sprintf("%s[%d]", currentPath, i)
|
||||
if isLastStep {
|
||||
*results = append(*results, JSONNode{Value: item, Path: itemPath})
|
||||
} else {
|
||||
err := traverseWithPaths(item, remainingSteps, results, itemPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", itemPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle specific index
|
||||
if step.Index >= 0 && step.Index < len(arr) {
|
||||
item := arr[step.Index]
|
||||
itemPath := fmt.Sprintf("%s[%d]", currentPath, step.Index)
|
||||
if isLastStep {
|
||||
*results = append(*results, JSONNode{Value: item, Path: itemPath})
|
||||
} else {
|
||||
err := traverseWithPaths(item, remainingSteps, results, itemPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", itemPath, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("index %d out of bounds for array at path: %s", step.Index, currentPath)
|
||||
}
|
||||
|
||||
case RecursiveDescentStep:
|
||||
// For recursive descent, first check direct match at this level
|
||||
if m, ok := node.(map[string]interface{}); ok && step.Key != "*" {
|
||||
if val, exists := m[step.Key]; exists {
|
||||
directPath := currentPath + "." + step.Key
|
||||
if isLastStep {
|
||||
*results = append(*results, JSONNode{Value: val, Path: directPath})
|
||||
} else {
|
||||
err := traverseWithPaths(val, remainingSteps, results, directPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", directPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For wildcard, collect this node
|
||||
if step.Key == "*" && isLastStep {
|
||||
*results = append(*results, JSONNode{Value: node, Path: currentPath})
|
||||
}
|
||||
|
||||
// Then continue recursion to all children
|
||||
switch n := node.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, v := range n {
|
||||
childPath := currentPath + "." + k
|
||||
err := traverseWithPaths(v, steps, results, childPath) // Use the same steps
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", childPath, err)
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for i, v := range n {
|
||||
childPath := fmt.Sprintf("%s[%d]", currentPath, i)
|
||||
err := traverseWithPaths(v, steps, results, childPath) // Use the same steps
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", childPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case WildcardStep:
|
||||
m, ok := node.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("node is not a map; actual type: %T", node)
|
||||
}
|
||||
|
||||
for k, v := range m {
|
||||
childPath := currentPath + "." + k
|
||||
if isLastStep {
|
||||
*results = append(*results, JSONNode{Value: v, Path: childPath})
|
||||
} else {
|
||||
err := traverseWithPaths(v, remainingSteps, results, childPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to traverse JSONPath %q: %w", childPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -1,577 +0,0 @@
|
||||
package jsonpath
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetWithPathsBasic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data map[string]interface{}
|
||||
path string
|
||||
expected []JSONNode
|
||||
error bool
|
||||
}{
|
||||
{
|
||||
name: "simple property",
|
||||
data: map[string]interface{}{
|
||||
"name": "John",
|
||||
"age": 30,
|
||||
},
|
||||
path: "$.name",
|
||||
expected: []JSONNode{
|
||||
{Value: "John", Path: "$.name"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested property",
|
||||
data: map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"name": "John",
|
||||
"age": 30,
|
||||
},
|
||||
},
|
||||
path: "$.user.name",
|
||||
expected: []JSONNode{
|
||||
{Value: "John", Path: "$.user.name"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "array access",
|
||||
data: map[string]interface{}{
|
||||
"users": []interface{}{
|
||||
map[string]interface{}{"name": "John", "age": 30},
|
||||
map[string]interface{}{"name": "Jane", "age": 25},
|
||||
},
|
||||
},
|
||||
path: "$.users[1].name",
|
||||
expected: []JSONNode{
|
||||
{Value: "Jane", Path: "$.users[1].name"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wildcard",
|
||||
data: map[string]interface{}{
|
||||
"users": []interface{}{
|
||||
map[string]interface{}{"name": "John", "age": 30},
|
||||
map[string]interface{}{"name": "Jane", "age": 25},
|
||||
},
|
||||
},
|
||||
path: "$.users[*].name",
|
||||
expected: []JSONNode{
|
||||
{Value: "John", Path: "$.users[0].name"},
|
||||
{Value: "Jane", Path: "$.users[1].name"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursive descent",
|
||||
data: map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"name": "John",
|
||||
"profile": map[string]interface{}{
|
||||
"email": "john@example.com",
|
||||
},
|
||||
},
|
||||
"admin": map[string]interface{}{
|
||||
"email": "admin@example.com",
|
||||
},
|
||||
},
|
||||
path: "$..email",
|
||||
expected: []JSONNode{
|
||||
{Value: "john@example.com", Path: "$.user.profile.email"},
|
||||
{Value: "admin@example.com", Path: "$.admin.email"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nonexistent path",
|
||||
data: map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"name": "John",
|
||||
},
|
||||
},
|
||||
path: "$.user.email",
|
||||
expected: []JSONNode{},
|
||||
error: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := Get(tt.data, tt.path)
|
||||
if err != nil {
|
||||
if !tt.error {
|
||||
t.Errorf("GetWithPaths() returned error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// For nonexistent path, we expect empty slice
|
||||
if tt.name == "nonexistent path" {
|
||||
if len(result) > 0 {
|
||||
t.Errorf("GetWithPaths() returned %v, expected empty result", result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check if lengths match
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("GetWithPaths() returned %d items, expected %d", len(result), len(tt.expected))
|
||||
return
|
||||
}
|
||||
|
||||
// For wildcard results, we need to check containment rather than exact order
|
||||
if tt.name == "wildcard" || tt.name == "recursive descent" {
|
||||
// For each expected item, check if it exists in the results by both value and path
|
||||
for _, expected := range tt.expected {
|
||||
found := false
|
||||
for _, r := range result {
|
||||
if reflect.DeepEqual(r.Value, expected.Value) && r.Path == expected.Path {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("GetWithPaths() missing expected value: %v with path: %s", expected.Value, expected.Path)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Otherwise check exact equality of both values and paths
|
||||
for i, expected := range tt.expected {
|
||||
if !reflect.DeepEqual(result[i].Value, expected.Value) {
|
||||
t.Errorf("GetWithPaths() value at [%d] = %v, expected %v", i, result[i].Value, expected.Value)
|
||||
}
|
||||
if result[i].Path != expected.Path {
|
||||
t.Errorf("GetWithPaths() path at [%d] = %s, expected %s", i, result[i].Path, expected.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
t.Run("simple property", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"name": "John",
|
||||
"age": 30,
|
||||
}
|
||||
err := Set(data, "$.name", "Jane")
|
||||
if err != nil {
|
||||
t.Errorf("Set() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if data["name"] != "Jane" {
|
||||
t.Errorf("Set() failed: expected name to be 'Jane', got %v", data["name"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nested property", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"name": "John",
|
||||
"age": 30,
|
||||
},
|
||||
}
|
||||
err := Set(data, "$.user.name", "Jane")
|
||||
if err != nil {
|
||||
t.Errorf("Set() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
user, ok := data["user"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("User is not a map")
|
||||
}
|
||||
if user["name"] != "Jane" {
|
||||
t.Errorf("Set() failed: expected user.name to be 'Jane', got %v", user["name"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("array element", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"users": []interface{}{
|
||||
map[string]interface{}{"name": "John", "age": 30},
|
||||
map[string]interface{}{"name": "Jane", "age": 25},
|
||||
},
|
||||
}
|
||||
err := Set(data, "$.users[0].name", "Bob")
|
||||
if err != nil {
|
||||
t.Errorf("Set() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
users, ok := data["users"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Users is not a slice")
|
||||
}
|
||||
user0, ok := users[0].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("User is not a map")
|
||||
}
|
||||
if user0["name"] != "Bob" {
|
||||
t.Errorf("Set() failed: expected users[0].name to be 'Bob', got %v", user0["name"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("complex value", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"name": "John",
|
||||
"profile": map[string]interface{}{
|
||||
"email": "john@example.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
newProfile := map[string]interface{}{
|
||||
"email": "john.doe@example.com",
|
||||
"phone": "123-456-7890",
|
||||
}
|
||||
|
||||
err := Set(data, "$.user.profile", newProfile)
|
||||
if err != nil {
|
||||
t.Errorf("Set() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
userMap, ok := data["user"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("User is not a map")
|
||||
}
|
||||
|
||||
profile, ok := userMap["profile"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Profile is not a map")
|
||||
}
|
||||
|
||||
if profile["email"] != "john.doe@example.com" || profile["phone"] != "123-456-7890" {
|
||||
t.Errorf("Set() failed: expected profile to be updated with new values")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create new property", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"name": "John",
|
||||
},
|
||||
}
|
||||
|
||||
err := Set(data, "$.user.email", "john@example.com")
|
||||
if err != nil {
|
||||
t.Errorf("Set() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
userMap, ok := data["user"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("User is not a map")
|
||||
}
|
||||
|
||||
if email, exists := userMap["email"]; !exists || email != "john@example.com" {
|
||||
t.Errorf("Set() failed: expected user.email to be 'john@example.com', got %v", userMap["email"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create nested properties", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"name": "John",
|
||||
},
|
||||
}
|
||||
|
||||
err := Set(data, "$.user.contact.email", "john@example.com")
|
||||
if err != nil {
|
||||
t.Errorf("Set() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
userMap, ok := data["user"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("User is not a map")
|
||||
}
|
||||
|
||||
contact, ok := userMap["contact"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Contact is not a map")
|
||||
}
|
||||
|
||||
if email, exists := contact["email"]; !exists || email != "john@example.com" {
|
||||
t.Errorf("Set() failed: expected user.contact.email to be 'john@example.com', got %v", contact["email"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("create array and element", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"name": "John",
|
||||
},
|
||||
}
|
||||
|
||||
// This should create an empty addresses array, but won't be able to set index 0
|
||||
// since the array is empty
|
||||
err := Set(data, "$.user.addresses[0].street", "123 Main St")
|
||||
if err != nil {
|
||||
t.Errorf("Set() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple targets (should only update first)", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"users": []interface{}{
|
||||
map[string]interface{}{"active": true},
|
||||
map[string]interface{}{"active": true},
|
||||
},
|
||||
}
|
||||
|
||||
err := Set(data, "$.users[*].active", false)
|
||||
if err != nil {
|
||||
t.Errorf("Set() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
users, ok := data["users"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Users is not a slice")
|
||||
}
|
||||
|
||||
user0, ok := users[0].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("User0 is not a map")
|
||||
}
|
||||
|
||||
user1, ok := users[1].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("User1 is not a map")
|
||||
}
|
||||
|
||||
// Only the first one should be changed
|
||||
if active, exists := user0["active"]; !exists || active != false {
|
||||
t.Errorf("Set() failed: expected users[0].active to be false, got %v", user0["active"])
|
||||
}
|
||||
|
||||
// The second one should remain unchanged
|
||||
if active, exists := user1["active"]; !exists || active != true {
|
||||
t.Errorf("Set() incorrectly modified users[1].active: expected true, got %v", user1["active"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("setting on root should not fail (anymore)", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"name": "John",
|
||||
}
|
||||
|
||||
err := Set(data, "$", "Jane")
|
||||
if err != nil {
|
||||
t.Errorf("Set() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Data should be unchanged
|
||||
if data["name"] != "John" {
|
||||
t.Errorf("Data was modified when setting on root")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetAll(t *testing.T) {
|
||||
t.Run("simple property", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"name": "John",
|
||||
"age": 30,
|
||||
}
|
||||
err := SetAll(data, "$.name", "Jane")
|
||||
if err != nil {
|
||||
t.Errorf("SetAll() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
if data["name"] != "Jane" {
|
||||
t.Errorf("SetAll() failed: expected name to be 'Jane', got %v", data["name"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("all array elements", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"users": []interface{}{
|
||||
map[string]interface{}{"active": true},
|
||||
map[string]interface{}{"active": true},
|
||||
},
|
||||
}
|
||||
|
||||
err := SetAll(data, "$.users[*].active", false)
|
||||
if err != nil {
|
||||
t.Errorf("SetAll() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
users, ok := data["users"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Users is not a slice")
|
||||
}
|
||||
|
||||
// Both elements should be updated
|
||||
for i, user := range users {
|
||||
userMap, ok := user.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("User%d is not a map", i)
|
||||
}
|
||||
|
||||
if active, exists := userMap["active"]; !exists || active != false {
|
||||
t.Errorf("SetAll() failed: expected users[%d].active to be false, got %v", i, userMap["active"])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("recursive descent", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"profile": map[string]interface{}{
|
||||
"active": true,
|
||||
},
|
||||
},
|
||||
"admin": map[string]interface{}{
|
||||
"profile": map[string]interface{}{
|
||||
"active": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := SetAll(data, "$..active", false)
|
||||
if err != nil {
|
||||
t.Errorf("SetAll() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check user profile
|
||||
userProfile, ok := data["user"].(map[string]interface{})["profile"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Failed to access user.profile")
|
||||
}
|
||||
if active, exists := userProfile["active"]; !exists || active != false {
|
||||
t.Errorf("SetAll() didn't update user.profile.active, got: %v", active)
|
||||
}
|
||||
|
||||
// Check admin profile
|
||||
adminProfile, ok := data["admin"].(map[string]interface{})["profile"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Failed to access admin.profile")
|
||||
}
|
||||
if active, exists := adminProfile["active"]; !exists || active != false {
|
||||
t.Errorf("SetAll() didn't update admin.profile.active, got: %v", active)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetWithPathsExtended(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data map[string]interface{}
|
||||
path string
|
||||
expected []JSONNode
|
||||
}{
|
||||
{
|
||||
name: "simple property",
|
||||
data: map[string]interface{}{
|
||||
"name": "John",
|
||||
"age": 30,
|
||||
},
|
||||
path: "$.name",
|
||||
expected: []JSONNode{
|
||||
{Value: "John", Path: "$.name"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested property",
|
||||
data: map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"name": "John",
|
||||
"age": 30,
|
||||
},
|
||||
},
|
||||
path: "$.user.name",
|
||||
expected: []JSONNode{
|
||||
{Value: "John", Path: "$.user.name"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "array access",
|
||||
data: map[string]interface{}{
|
||||
"users": []interface{}{
|
||||
map[string]interface{}{"name": "John", "age": 30},
|
||||
map[string]interface{}{"name": "Jane", "age": 25},
|
||||
},
|
||||
},
|
||||
path: "$.users[1].name",
|
||||
expected: []JSONNode{
|
||||
{Value: "Jane", Path: "$.users[1].name"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wildcard",
|
||||
data: map[string]interface{}{
|
||||
"users": []interface{}{
|
||||
map[string]interface{}{"name": "John", "age": 30},
|
||||
map[string]interface{}{"name": "Jane", "age": 25},
|
||||
},
|
||||
},
|
||||
path: "$.users[*].name",
|
||||
expected: []JSONNode{
|
||||
{Value: "John", Path: "$.users[0].name"},
|
||||
{Value: "Jane", Path: "$.users[1].name"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursive descent",
|
||||
data: map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"name": "John",
|
||||
"profile": map[string]interface{}{
|
||||
"email": "john@example.com",
|
||||
},
|
||||
},
|
||||
"admin": map[string]interface{}{
|
||||
"email": "admin@example.com",
|
||||
},
|
||||
},
|
||||
path: "$..email",
|
||||
expected: []JSONNode{
|
||||
{Value: "john@example.com", Path: "$.user.profile.email"},
|
||||
{Value: "admin@example.com", Path: "$.admin.email"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := Get(tt.data, tt.path)
|
||||
if err != nil {
|
||||
t.Errorf("GetWithPaths() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if lengths match
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("GetWithPaths() returned %d items, expected %d", len(result), len(tt.expected))
|
||||
return
|
||||
}
|
||||
|
||||
// For each expected item, find its match in the results and verify both value and path
|
||||
for _, expected := range tt.expected {
|
||||
found := false
|
||||
for _, r := range result {
|
||||
// Check if value matches
|
||||
if reflect.DeepEqual(r.Value, expected.Value) {
|
||||
found = true
|
||||
// Check if path matches
|
||||
if r.Path != expected.Path {
|
||||
t.Errorf("Path mismatch for value %v: got %s, expected %s", r.Value, r.Path, expected.Path)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected node with value %v and path %s not found in results", expected.Value, expected.Path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -1,318 +0,0 @@
|
||||
package jsonpath
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testData = map[string]interface{}{
|
||||
"store": map[string]interface{}{
|
||||
"book": []interface{}{
|
||||
map[string]interface{}{
|
||||
"title": "The Fellowship of the Ring",
|
||||
"price": 22.99,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"title": "The Two Towers",
|
||||
"price": 23.45,
|
||||
},
|
||||
},
|
||||
"bicycle": map[string]interface{}{
|
||||
"color": "red",
|
||||
"price": 199.95,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
steps []JSONStep
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
path: "$.store.bicycle.color",
|
||||
steps: []JSONStep{
|
||||
{Type: RootStep},
|
||||
{Type: ChildStep, Key: "store"},
|
||||
{Type: ChildStep, Key: "bicycle"},
|
||||
{Type: ChildStep, Key: "color"},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "$..price",
|
||||
steps: []JSONStep{
|
||||
{Type: RootStep},
|
||||
{Type: RecursiveDescentStep, Key: "price"},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "$.store.book[*].title",
|
||||
steps: []JSONStep{
|
||||
{Type: RootStep},
|
||||
{Type: ChildStep, Key: "store"},
|
||||
{Type: ChildStep, Key: "book"},
|
||||
{Type: IndexStep, Index: -1}, // Wildcard
|
||||
{Type: ChildStep, Key: "title"},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "$.store.book[0]",
|
||||
steps: []JSONStep{
|
||||
{Type: RootStep},
|
||||
{Type: ChildStep, Key: "store"},
|
||||
{Type: ChildStep, Key: "book"},
|
||||
{Type: IndexStep, Index: 0},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "invalid.path",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
path: "$.store.book[abc]",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.path, func(t *testing.T) {
|
||||
steps, err := ParseJSONPath(tt.path)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("ParseJSONPath() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if !tt.wantErr && !reflect.DeepEqual(steps, tt.steps) {
|
||||
t.Errorf("ParseJSONPath() steps = %+v, want %+v", steps, tt.steps)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluator(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expected []JSONNode
|
||||
error bool
|
||||
}{
|
||||
{
|
||||
name: "simple_property_access",
|
||||
path: "$.store.bicycle.color",
|
||||
expected: []JSONNode{
|
||||
{Value: "red", Path: "$.store.bicycle.color"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "array_index_access",
|
||||
path: "$.store.book[0].title",
|
||||
expected: []JSONNode{
|
||||
{Value: "The Fellowship of the Ring", Path: "$.store.book[0].title"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wildcard_array_access",
|
||||
path: "$.store.book[*].title",
|
||||
expected: []JSONNode{
|
||||
{Value: "The Fellowship of the Ring", Path: "$.store.book[0].title"},
|
||||
{Value: "The Two Towers", Path: "$.store.book[1].title"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursive_price_search",
|
||||
path: "$..price",
|
||||
expected: []JSONNode{
|
||||
{Value: 22.99, Path: "$.store.book[0].price"},
|
||||
{Value: 23.45, Path: "$.store.book[1].price"},
|
||||
{Value: 199.95, Path: "$.store.bicycle.price"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wildcard_recursive",
|
||||
path: "$..*",
|
||||
expected: []JSONNode{
|
||||
// These will be compared by value only, paths will be validated separately
|
||||
{Value: testData["store"].(map[string]interface{})["book"]},
|
||||
{Value: testData["store"].(map[string]interface{})["bicycle"]},
|
||||
{Value: testData["store"].(map[string]interface{})["book"].([]interface{})[0]},
|
||||
{Value: testData["store"].(map[string]interface{})["book"].([]interface{})[1]},
|
||||
{Value: "The Fellowship of the Ring"},
|
||||
{Value: 22.99},
|
||||
{Value: "The Two Towers"},
|
||||
{Value: 23.45},
|
||||
{Value: "red"},
|
||||
{Value: 199.95},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid_index",
|
||||
path: "$.store.book[5]",
|
||||
expected: []JSONNode{},
|
||||
error: true,
|
||||
},
|
||||
{
|
||||
name: "nonexistent_property",
|
||||
path: "$.store.nonexistent",
|
||||
expected: []JSONNode{},
|
||||
error: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Use GetWithPaths directly
|
||||
result, err := Get(testData, tt.path)
|
||||
if err != nil {
|
||||
if !tt.error {
|
||||
t.Errorf("Get() returned error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Special handling for wildcard recursive test
|
||||
if tt.name == "wildcard_recursive" {
|
||||
// Skip length check for wildcard recursive since it might vary
|
||||
// Just verify that each expected item is in the results
|
||||
|
||||
// Validate values match and paths are filled in
|
||||
for _, e := range tt.expected {
|
||||
found := false
|
||||
for _, r := range result {
|
||||
if reflect.DeepEqual(r.Value, e.Value) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected value %v not found in results", e.Value)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("Expected %d items, got %d", len(tt.expected), len(result))
|
||||
}
|
||||
|
||||
// Validate both values and paths
|
||||
for i, e := range tt.expected {
|
||||
if i < len(result) {
|
||||
if !reflect.DeepEqual(result[i].Value, e.Value) {
|
||||
t.Errorf("Value at [%d]: got %v, expected %v", i, result[i].Value, e.Value)
|
||||
}
|
||||
if result[i].Path != e.Path {
|
||||
t.Errorf("Path at [%d]: got %s, expected %s", i, result[i].Path, e.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
t.Run("empty_data", func(t *testing.T) {
|
||||
result, err := Get(nil, "$.a.b")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for empty data")
|
||||
return
|
||||
}
|
||||
if len(result) > 0 {
|
||||
t.Errorf("Expected empty result, got %v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty_path", func(t *testing.T) {
|
||||
_, err := ParseJSONPath("")
|
||||
if err == nil {
|
||||
t.Error("Expected error for empty path")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("numeric_keys", func(t *testing.T) {
|
||||
data := map[string]interface{}{
|
||||
"42": "answer",
|
||||
}
|
||||
result, err := Get(data, "$.42")
|
||||
if err != nil {
|
||||
t.Errorf("Get() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
if len(result) == 0 || result[0].Value != "answer" {
|
||||
t.Errorf("Expected 'answer', got %v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetWithPaths(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expected []JSONNode
|
||||
}{
|
||||
{
|
||||
name: "simple_property_access",
|
||||
path: "$.store.bicycle.color",
|
||||
expected: []JSONNode{
|
||||
{Value: "red", Path: "$.store.bicycle.color"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "array_index_access",
|
||||
path: "$.store.book[0].title",
|
||||
expected: []JSONNode{
|
||||
{Value: "The Fellowship of the Ring", Path: "$.store.book[0].title"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wildcard_array_access",
|
||||
path: "$.store.book[*].title",
|
||||
expected: []JSONNode{
|
||||
{Value: "The Fellowship of the Ring", Path: "$.store.book[0].title"},
|
||||
{Value: "The Two Towers", Path: "$.store.book[1].title"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recursive_price_search",
|
||||
path: "$..price",
|
||||
expected: []JSONNode{
|
||||
{Value: 22.99, Path: "$.store.book[0].price"},
|
||||
{Value: 23.45, Path: "$.store.book[1].price"},
|
||||
{Value: 199.95, Path: "$.store.bicycle.price"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := Get(testData, tt.path)
|
||||
if err != nil {
|
||||
t.Errorf("Get() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if lengths match
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("GetWithPaths() returned %d items, expected %d", len(result), len(tt.expected))
|
||||
return
|
||||
}
|
||||
|
||||
// For each expected item, find its match in the results and verify both value and path
|
||||
for _, expected := range tt.expected {
|
||||
found := false
|
||||
for _, r := range result {
|
||||
// First verify the value matches
|
||||
if reflect.DeepEqual(r.Value, expected.Value) {
|
||||
found = true
|
||||
// Then verify the path matches
|
||||
if r.Path != expected.Path {
|
||||
t.Errorf("Path mismatch for value %v: got %s, expected %s", r.Value, r.Path, expected.Path)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected node with value %v and path %s not found in results", expected.Value, expected.Path)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -2,137 +2,107 @@ package processor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/antchfx/xmlquery"
|
||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// Processor defines the interface for all file processors
|
||||
type Processor interface {
|
||||
// Process handles processing a file with the given pattern and Lua expression
|
||||
// Now implemented as a base function in processor.go
|
||||
// Process(filename string, pattern string, luaExpr string) (int, int, error)
|
||||
|
||||
// ProcessContent handles processing a string content directly with the given pattern and Lua expression
|
||||
// Returns the modified content, modification count, match count, and any error
|
||||
ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error)
|
||||
|
||||
// ToLua converts processor-specific data to Lua variables
|
||||
ToLua(L *lua.LState, data interface{}) error
|
||||
|
||||
// FromLua retrieves modified data from Lua
|
||||
FromLua(L *lua.LState) (interface{}, error)
|
||||
}
|
||||
|
||||
// ModificationRecord tracks a single value modification
|
||||
type ModificationRecord struct {
|
||||
File string
|
||||
OldValue string
|
||||
NewValue string
|
||||
Operation string
|
||||
Context string
|
||||
}
|
||||
// Maybe we make this an interface again for the shits and giggles
|
||||
// We will see, it could easily be...
|
||||
|
||||
func NewLuaState() (*lua.LState, error) {
|
||||
L := lua.NewState()
|
||||
// defer L.Close()
|
||||
|
||||
// Load math library
|
||||
logger.Debug("Loading Lua math library")
|
||||
L.Push(L.GetGlobal("require"))
|
||||
L.Push(lua.LString("math"))
|
||||
if err := L.PCall(1, 1, nil); err != nil {
|
||||
logger.Error("Failed to load Lua math library: %v", err)
|
||||
return nil, fmt.Errorf("error loading Lua math library: %v", err)
|
||||
}
|
||||
|
||||
// Initialize helper functions
|
||||
logger.Debug("Initializing Lua helper functions")
|
||||
if err := InitLuaHelpers(L); err != nil {
|
||||
logger.Error("Failed to initialize Lua helper functions: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return L, nil
|
||||
}
|
||||
|
||||
func Process(p Processor, filename string, pattern string, luaExpr string) (int, int, error) {
|
||||
// Read file content
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("error getting current working directory: %v", err)
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(cwd, filename)
|
||||
content, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("error reading file: %v", err)
|
||||
}
|
||||
|
||||
fileContent := string(content)
|
||||
|
||||
// Process the content
|
||||
modifiedContent, modCount, matchCount, err := p.ProcessContent(fileContent, pattern, luaExpr)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// If we made modifications, save the file
|
||||
if modCount > 0 {
|
||||
err = os.WriteFile(fullPath, []byte(modifiedContent), 0644)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("error writing file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return modCount, matchCount, nil
|
||||
}
|
||||
|
||||
// ToLua converts a struct or map to a Lua table recursively
|
||||
func ToLua(L *lua.LState, data interface{}) (lua.LValue, error) {
|
||||
switch v := data.(type) {
|
||||
case *xmlquery.Node:
|
||||
luaTable := L.NewTable()
|
||||
luaTable.RawSetString("text", lua.LString(v.Data))
|
||||
// Should be a map, simple key value pairs
|
||||
attr, err := ToLua(L, v.Attr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
luaTable.RawSetString("attr", attr)
|
||||
return luaTable, nil
|
||||
case map[string]interface{}:
|
||||
luaTable := L.NewTable()
|
||||
for key, value := range v {
|
||||
luaValue, err := ToLua(L, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
luaTable.RawSetString(key, luaValue)
|
||||
}
|
||||
return luaTable, nil
|
||||
case []interface{}:
|
||||
luaTable := L.NewTable()
|
||||
for i, value := range v {
|
||||
luaValue, err := ToLua(L, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
luaTable.RawSetInt(i+1, luaValue) // Lua arrays are 1-indexed
|
||||
}
|
||||
return luaTable, nil
|
||||
case string:
|
||||
return lua.LString(v), nil
|
||||
case bool:
|
||||
return lua.LBool(v), nil
|
||||
case float64:
|
||||
return lua.LNumber(v), nil
|
||||
case nil:
|
||||
return lua.LNil, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported data type: %T", data)
|
||||
}
|
||||
}
|
||||
// func Process(filename string, pattern string, luaExpr string) (int, int, error) {
|
||||
// logger.Debug("Processing file %q with pattern %q", filename, pattern)
|
||||
//
|
||||
// // Read file content
|
||||
// cwd, err := os.Getwd()
|
||||
// if err != nil {
|
||||
// logger.Error("Failed to get current working directory: %v", err)
|
||||
// return 0, 0, fmt.Errorf("error getting current working directory: %v", err)
|
||||
// }
|
||||
//
|
||||
// fullPath := filepath.Join(cwd, filename)
|
||||
// logger.Trace("Reading file from: %s", fullPath)
|
||||
//
|
||||
// stat, err := os.Stat(fullPath)
|
||||
// if err != nil {
|
||||
// logger.Error("Failed to stat file %s: %v", fullPath, err)
|
||||
// return 0, 0, fmt.Errorf("error getting file info: %v", err)
|
||||
// }
|
||||
// logger.Debug("File size: %d bytes, modified: %s", stat.Size(), stat.ModTime().Format(time.RFC3339))
|
||||
//
|
||||
// content, err := os.ReadFile(fullPath)
|
||||
// if err != nil {
|
||||
// logger.Error("Failed to read file %s: %v", fullPath, err)
|
||||
// return 0, 0, fmt.Errorf("error reading file: %v", err)
|
||||
// }
|
||||
//
|
||||
// fileContent := string(content)
|
||||
// logger.Trace("File read successfully: %d bytes, hash: %x", len(content), md5sum(content))
|
||||
//
|
||||
// // Detect and log file type
|
||||
// fileType := detectFileType(filename, fileContent)
|
||||
// if fileType != "" {
|
||||
// logger.Debug("Detected file type: %s", fileType)
|
||||
// }
|
||||
//
|
||||
// // Process the content
|
||||
// logger.Debug("Starting content processing")
|
||||
// modifiedContent, modCount, matchCount, err := ProcessContent(fileContent, pattern, luaExpr)
|
||||
// if err != nil {
|
||||
// logger.Error("Processing error: %v", err)
|
||||
// return 0, 0, err
|
||||
// }
|
||||
//
|
||||
// logger.Debug("Processing results: %d matches, %d modifications", matchCount, modCount)
|
||||
//
|
||||
// // If we made modifications, save the file
|
||||
// if modCount > 0 {
|
||||
// // Calculate changes summary
|
||||
// changePercent := float64(len(modifiedContent)) / float64(len(fileContent)) * 100
|
||||
// logger.Info("File size change: %d → %d bytes (%.1f%%)",
|
||||
// len(fileContent), len(modifiedContent), changePercent)
|
||||
//
|
||||
// logger.Debug("Writing modified content to %s", fullPath)
|
||||
// err = os.WriteFile(fullPath, []byte(modifiedContent), 0644)
|
||||
// if err != nil {
|
||||
// logger.Error("Failed to write to file %s: %v", fullPath, err)
|
||||
// return 0, 0, fmt.Errorf("error writing file: %v", err)
|
||||
// }
|
||||
// logger.Debug("File written successfully, new hash: %x", md5sum([]byte(modifiedContent)))
|
||||
// } else if matchCount > 0 {
|
||||
// logger.Debug("No content modifications needed for %d matches", matchCount)
|
||||
// } else {
|
||||
// logger.Debug("No matches found in file")
|
||||
// }
|
||||
//
|
||||
// return modCount, matchCount, nil
|
||||
// }
|
||||
|
||||
// FromLua converts a Lua table to a struct or map recursively
|
||||
func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) {
|
||||
@@ -174,25 +144,28 @@ func FromLua(L *lua.LState, luaValue lua.LValue) (interface{}, error) {
|
||||
}
|
||||
|
||||
func IsLuaTableArray(L *lua.LState, v *lua.LTable) (bool, error) {
|
||||
logger.Trace("Checking if Lua table is an array")
|
||||
L.SetGlobal("table_to_check", v)
|
||||
|
||||
// Use our predefined helper function from InitLuaHelpers
|
||||
err := L.DoString(`is_array = isArray(table_to_check)`)
|
||||
if err != nil {
|
||||
logger.Error("Error determining if table is an array: %v", err)
|
||||
return false, fmt.Errorf("error determining if table is array: %w", err)
|
||||
}
|
||||
|
||||
// Check the result of our Lua function
|
||||
isArray := L.GetGlobal("is_array")
|
||||
// LVIsFalse returns true if a given LValue is a nil or false otherwise false.
|
||||
if !lua.LVIsFalse(isArray) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
result := !lua.LVIsFalse(isArray)
|
||||
logger.Trace("Lua table is array: %v", result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// InitLuaHelpers initializes common Lua helper functions
|
||||
func InitLuaHelpers(L *lua.LState) error {
|
||||
logger.Debug("Loading Lua helper functions")
|
||||
|
||||
helperScript := `
|
||||
-- Custom Lua helpers for math operations
|
||||
function min(a, b) return math.min(a, b) end
|
||||
@@ -205,6 +178,40 @@ function floor(x) return math.floor(x) end
|
||||
function ceil(x) return math.ceil(x) end
|
||||
function upper(s) return string.upper(s) end
|
||||
function lower(s) return string.lower(s) end
|
||||
function format(s, ...) return string.format(s, ...) end
|
||||
function trim(s) return string.gsub(s, "^%s*(.-)%s*$", "%1") end
|
||||
|
||||
-- String split helper
|
||||
function strsplit(inputstr, sep)
|
||||
if sep == nil then
|
||||
sep = "%s"
|
||||
end
|
||||
local t = {}
|
||||
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
|
||||
table.insert(t, str)
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
---@param table table
|
||||
---@param depth number?
|
||||
function DumpTable(table, depth)
|
||||
if depth == nil then
|
||||
depth = 0
|
||||
end
|
||||
if (depth > 200) then
|
||||
print("Error: Depth > 200 in dumpTable()")
|
||||
return
|
||||
end
|
||||
for k, v in pairs(table) do
|
||||
if (type(v) == "table") then
|
||||
print(string.rep(" ", depth) .. k .. ":")
|
||||
DumpTable(v, depth + 1)
|
||||
else
|
||||
print(string.rep(" ", depth) .. k .. ": ", v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- String to number conversion helper
|
||||
function num(str)
|
||||
@@ -238,15 +245,16 @@ end
|
||||
modified = false
|
||||
`
|
||||
if err := L.DoString(helperScript); err != nil {
|
||||
logger.Error("Failed to load Lua helper functions: %v", err)
|
||||
return fmt.Errorf("error loading helper functions: %v", err)
|
||||
}
|
||||
|
||||
logger.Debug("Setting up Lua print function to Go")
|
||||
L.SetGlobal("print", L.NewFunction(printToGo))
|
||||
L.SetGlobal("fetch", L.NewFunction(fetch))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper utility functions
|
||||
|
||||
// LimitString truncates a string to maxLen and adds "..." if truncated
|
||||
func LimitString(s string, maxLen int) string {
|
||||
s = strings.ReplaceAll(s, "\n", "\\n")
|
||||
@@ -279,6 +287,8 @@ func PrependLuaAssignment(luaExpr string) string {
|
||||
|
||||
// BuildLuaScript prepares a Lua expression from shorthand notation
|
||||
func BuildLuaScript(luaExpr string) string {
|
||||
logger.Debug("Building Lua script from expression: %s", luaExpr)
|
||||
|
||||
luaExpr = PrependLuaAssignment(luaExpr)
|
||||
|
||||
// This allows the user to specify whether or not they modified a value
|
||||
@@ -299,31 +309,108 @@ func BuildLuaScript(luaExpr string) string {
|
||||
}
|
||||
|
||||
func printToGo(L *lua.LState) int {
|
||||
// Get the number of arguments passed to the Lua print function
|
||||
n := L.GetTop()
|
||||
// Create a slice to hold the arguments
|
||||
args := make([]interface{}, n)
|
||||
for i := 1; i <= n; i++ {
|
||||
args[i-1] = L.Get(i) // Get the argument from Lua stack
|
||||
top := L.GetTop()
|
||||
|
||||
args := make([]interface{}, top)
|
||||
for i := 1; i <= top; i++ {
|
||||
args[i-1] = L.Get(i)
|
||||
}
|
||||
// Print the arguments to Go's stdout
|
||||
log.Print("Lua: ")
|
||||
log.Println(args...)
|
||||
return 0 // No return values
|
||||
|
||||
// Format the message with proper spacing between arguments
|
||||
var parts []string
|
||||
for _, arg := range args {
|
||||
parts = append(parts, fmt.Sprintf("%v", arg))
|
||||
}
|
||||
message := strings.Join(parts, " ")
|
||||
|
||||
// Use the LUA log level with a script tag
|
||||
logger.Lua("%s", message)
|
||||
return 0
|
||||
}
|
||||
|
||||
// Max returns the maximum of two integers
|
||||
func Max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
func fetch(L *lua.LState) int {
|
||||
// Get URL from first argument
|
||||
url := L.ToString(1)
|
||||
if url == "" {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString("URL is required"))
|
||||
return 2
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Min returns the minimum of two integers
|
||||
func Min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
// Get options from second argument if provided
|
||||
var method string = "GET"
|
||||
var headers map[string]string = make(map[string]string)
|
||||
var body string = ""
|
||||
|
||||
if L.GetTop() > 1 {
|
||||
options := L.ToTable(2)
|
||||
if options != nil {
|
||||
// Get method
|
||||
if methodVal := options.RawGetString("method"); methodVal != lua.LNil {
|
||||
method = methodVal.String()
|
||||
}
|
||||
return b
|
||||
|
||||
// Get headers
|
||||
if headersVal := options.RawGetString("headers"); headersVal != lua.LNil {
|
||||
if headersTable, ok := headersVal.(*lua.LTable); ok {
|
||||
headersTable.ForEach(func(key lua.LValue, value lua.LValue) {
|
||||
headers[key.String()] = value.String()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Get body
|
||||
if bodyVal := options.RawGetString("body"); bodyVal != lua.LNil {
|
||||
body = bodyVal.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequest(method, url, strings.NewReader(body))
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("Error creating request: %v", err)))
|
||||
return 2
|
||||
}
|
||||
|
||||
// Set headers
|
||||
for key, value := range headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
// Make request
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("Error making request: %v", err)))
|
||||
return 2
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read response body
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
L.Push(lua.LNil)
|
||||
L.Push(lua.LString(fmt.Sprintf("Error reading response: %v", err)))
|
||||
return 2
|
||||
}
|
||||
|
||||
// Create response table
|
||||
responseTable := L.NewTable()
|
||||
responseTable.RawSetString("status", lua.LNumber(resp.StatusCode))
|
||||
responseTable.RawSetString("statusText", lua.LString(resp.Status))
|
||||
responseTable.RawSetString("ok", lua.LBool(resp.StatusCode >= 200 && resp.StatusCode < 300))
|
||||
responseTable.RawSetString("body", lua.LString(string(bodyBytes)))
|
||||
|
||||
// Set headers in response
|
||||
headersTable := L.NewTable()
|
||||
for key, values := range resp.Header {
|
||||
headersTable.RawSetString(key, lua.LString(values[0]))
|
||||
}
|
||||
responseTable.RawSetString("headers", headersTable)
|
||||
|
||||
L.Push(responseTable)
|
||||
return 1
|
||||
}
|
||||
|
@@ -1,90 +1,286 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"cook/utils"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// RegexProcessor implements the Processor interface using regex patterns
|
||||
type RegexProcessor struct{}
|
||||
|
||||
// ToLua sets capture groups as Lua variables (v1, v2, etc. for numeric values and s1, s2, etc. for strings)
|
||||
func (p *RegexProcessor) ToLua(L *lua.LState, data interface{}) error {
|
||||
captures, ok := data.([]string)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected []string for captures, got %T", data)
|
||||
}
|
||||
|
||||
// Set variables for each capture group, starting from v1/s1 for the first capture
|
||||
for i := 0; i < len(captures); i++ {
|
||||
// Set string version (always available as s1, s2, etc.)
|
||||
L.SetGlobal(fmt.Sprintf("s%d", i+1), lua.LString(captures[i]))
|
||||
|
||||
// Try to convert to number and set v1, v2, etc.
|
||||
if val, err := strconv.ParseFloat(captures[i], 64); err == nil {
|
||||
L.SetGlobal(fmt.Sprintf("v%d", i+1), lua.LNumber(val))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FromLua implements the Processor interface for RegexProcessor
|
||||
func (p *RegexProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
||||
// Get the modified values after Lua execution
|
||||
modifications := make(map[int]string)
|
||||
|
||||
// Check for modifications to v1-v12 and s1-s12
|
||||
for i := 0; i < 12; i++ {
|
||||
// Check both v and s variables to see if any were modified
|
||||
vVarName := fmt.Sprintf("v%d", i+1)
|
||||
sVarName := fmt.Sprintf("s%d", i+1)
|
||||
|
||||
vLuaVal := L.GetGlobal(vVarName)
|
||||
sLuaVal := L.GetGlobal(sVarName)
|
||||
|
||||
// If our value is a number then it's very likely we want it to be a number
|
||||
// And not a string
|
||||
// If we do want it to be a string we will cast it into a string in lua
|
||||
// wait that wouldn't work... Casting v to a string would not load it here
|
||||
if vLuaVal.Type() == lua.LTNumber {
|
||||
modifications[i] = vLuaVal.String()
|
||||
continue
|
||||
}
|
||||
if sLuaVal.Type() == lua.LTString {
|
||||
modifications[i] = sLuaVal.String()
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return modifications, nil
|
||||
}
|
||||
|
||||
type NamedCapture struct {
|
||||
type CaptureGroup struct {
|
||||
Name string
|
||||
Value string
|
||||
Updated string
|
||||
Range [2]int
|
||||
}
|
||||
|
||||
// ProcessContent applies regex replacement with Lua processing
|
||||
func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr string) (string, int, int, error) {
|
||||
// The filename here exists ONLY so we can pass it to the lua environment
|
||||
// It's not used for anything else
|
||||
func ProcessRegex(content string, command utils.ModifyCommand, filename string) ([]utils.ReplaceCommand, error) {
|
||||
var commands []utils.ReplaceCommand
|
||||
logger.Trace("Processing regex: %q", command.Regex)
|
||||
|
||||
// Start timing the regex processing
|
||||
startTime := time.Now()
|
||||
|
||||
// We don't HAVE to do this multiple times for a pattern
|
||||
// But it's quick enough for us to not care
|
||||
pattern := resolveRegexPlaceholders(command.Regex)
|
||||
// I'm not too happy about having to trim regex, we could have meaningful whitespace or newlines
|
||||
// But it's a compromise that allows us to use | in yaml
|
||||
// Otherwise we would have to escape every god damn pair of quotation marks
|
||||
// And a bunch of other shit
|
||||
pattern = strings.TrimSpace(pattern)
|
||||
logger.Debug("Compiling regex pattern: %s", pattern)
|
||||
|
||||
patternCompileStart := time.Now()
|
||||
compiledPattern, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
logger.Error("Error compiling pattern: %v", err)
|
||||
return commands, fmt.Errorf("error compiling pattern: %v", err)
|
||||
}
|
||||
logger.Debug("Compiled pattern successfully in %v: %s", time.Since(patternCompileStart), pattern)
|
||||
|
||||
// Same here, it's just string concatenation, it won't kill us
|
||||
// More important is that we don't fuck up the command
|
||||
// But we shouldn't be able to since it's passed by value
|
||||
previous := command.Lua
|
||||
luaExpr := BuildLuaScript(command.Lua)
|
||||
logger.Debug("Transformed Lua expression: %q → %q", previous, luaExpr)
|
||||
|
||||
// Process all regex matches
|
||||
matchFindStart := time.Now()
|
||||
indices := compiledPattern.FindAllStringSubmatchIndex(content, -1)
|
||||
matchFindDuration := time.Since(matchFindStart)
|
||||
|
||||
logger.Debug("Found %d matches in content of length %d (search took %v)",
|
||||
len(indices), len(content), matchFindDuration)
|
||||
|
||||
// Log pattern complexity metrics
|
||||
patternComplexity := estimatePatternComplexity(pattern)
|
||||
logger.Debug("Pattern complexity estimate: %d", patternComplexity)
|
||||
|
||||
if len(indices) == 0 {
|
||||
logger.Warning("No matches found for regex: %q", pattern)
|
||||
logger.Debug("Total regex processing time: %v", time.Since(startTime))
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
// We walk backwards because we're replacing something with something else that might be longer
|
||||
// And in the case it is longer than the original all indicces past that change will be fucked up
|
||||
// By going backwards we fuck up all the indices to the end of the file that we don't care about
|
||||
// Because there either aren't any (last match) or they're already modified (subsequent matches)
|
||||
for i, matchIndices := range indices {
|
||||
logger.Debug("Processing match %d of %d", i+1, len(indices))
|
||||
logger.Trace("Match indices: %v (match position %d-%d)", matchIndices, matchIndices[0], matchIndices[1])
|
||||
|
||||
L, err := NewLuaState()
|
||||
if err != nil {
|
||||
logger.Error("Error creating Lua state: %v", err)
|
||||
return commands, fmt.Errorf("error creating Lua state: %v", err)
|
||||
}
|
||||
L.SetGlobal("file", lua.LString(filename))
|
||||
// Hmm... Maybe we don't want to defer this..
|
||||
// Maybe we want to close them every iteration
|
||||
// We'll leave it as is for now
|
||||
defer L.Close()
|
||||
logger.Trace("Lua state created successfully for match %d", i+1)
|
||||
|
||||
// Why we're doing this whole song and dance of indices is to properly handle empty matches
|
||||
// Plus it's a little cleaner to surgically replace our matches
|
||||
// If we were to use string.replace and encountered an empty match there'd be nothing to replace
|
||||
// But using indices an empty match would have its starting and ending indices be the same
|
||||
// So when we're cutting open the array we say 0:7 + modified + 7:end
|
||||
// As if concatenating in the middle of the array
|
||||
// Plus it supports lookarounds
|
||||
match := content[matchIndices[0]:matchIndices[1]]
|
||||
matchPreview := match
|
||||
if len(match) > 50 {
|
||||
matchPreview = match[:47] + "..."
|
||||
}
|
||||
logger.Trace("Matched content: %q (length: %d)", matchPreview, len(match))
|
||||
|
||||
groups := matchIndices[2:]
|
||||
if len(groups) <= 0 {
|
||||
logger.Warning("No capture groups found for match %q and regex %q", matchPreview, pattern)
|
||||
continue
|
||||
}
|
||||
if len(groups)%2 == 1 {
|
||||
logger.Warning("Invalid number of group indices (%d), should be even: %v", len(groups), groups)
|
||||
continue
|
||||
}
|
||||
|
||||
// Count how many valid groups we have
|
||||
validGroups := 0
|
||||
for j := 0; j < len(groups); j += 2 {
|
||||
if groups[j] != -1 && groups[j+1] != -1 {
|
||||
validGroups++
|
||||
}
|
||||
}
|
||||
logger.Debug("Found %d valid capture groups in match", validGroups)
|
||||
|
||||
for _, index := range groups {
|
||||
if index == -1 {
|
||||
logger.Warning("Negative index encountered in match indices %v. This may indicate an issue with the regex pattern or an empty/optional capture group.", matchIndices)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// We have to use array to preserve order
|
||||
// Very important for the reconstruction step
|
||||
// Because we must overwrite the values in reverse order
|
||||
// See comments a few dozen lines above for more details
|
||||
captureGroups := make([]*CaptureGroup, 0, len(groups)/2)
|
||||
groupNames := compiledPattern.SubexpNames()[1:]
|
||||
for i, name := range groupNames {
|
||||
start := groups[i*2]
|
||||
end := groups[i*2+1]
|
||||
if start == -1 || end == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
value := content[start:end]
|
||||
captureGroups = append(captureGroups, &CaptureGroup{
|
||||
Name: name,
|
||||
Value: value,
|
||||
Range: [2]int{start, end},
|
||||
})
|
||||
|
||||
// Include name info in log if available
|
||||
if name != "" {
|
||||
logger.Trace("Capture group '%s': %q (pos %d-%d)", name, value, start, end)
|
||||
} else {
|
||||
logger.Trace("Capture group #%d: %q (pos %d-%d)", i+1, value, start, end)
|
||||
}
|
||||
}
|
||||
|
||||
// Use the DeduplicateGroups flag to control whether to deduplicate capture groups
|
||||
if !command.NoDedup {
|
||||
logger.Debug("Deduplicating capture groups as specified in command settings")
|
||||
captureGroups = deduplicateGroups(captureGroups)
|
||||
}
|
||||
|
||||
if err := toLua(L, captureGroups); err != nil {
|
||||
logger.Error("Failed to set Lua variables: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.Trace("Set %d capture groups as Lua variables", len(captureGroups))
|
||||
|
||||
if err := L.DoString(luaExpr); err != nil {
|
||||
logger.Error("Lua script execution failed: %v\nScript: %s\nCapture Groups: %+v",
|
||||
err, luaExpr, captureGroups)
|
||||
continue
|
||||
}
|
||||
logger.Trace("Lua script executed successfully")
|
||||
|
||||
// Get modifications from Lua
|
||||
captureGroups, err = fromLua(L, captureGroups)
|
||||
if err != nil {
|
||||
logger.Error("Failed to retrieve modifications from Lua: %v", err)
|
||||
continue
|
||||
}
|
||||
logger.Trace("Retrieved updated values from Lua")
|
||||
|
||||
replacement := ""
|
||||
replacementVar := L.GetGlobal("replacement")
|
||||
if replacementVar.Type() != lua.LTNil {
|
||||
replacement = replacementVar.String()
|
||||
logger.Debug("Using global replacement: %q", replacement)
|
||||
}
|
||||
|
||||
// Check if modification flag is set
|
||||
modifiedVal := L.GetGlobal("modified")
|
||||
if modifiedVal.Type() != lua.LTBool || !lua.LVAsBool(modifiedVal) {
|
||||
logger.Debug("Skipping match - no modifications made by Lua script")
|
||||
continue
|
||||
}
|
||||
|
||||
if replacement == "" {
|
||||
// Apply the modifications to the original match
|
||||
replacement = match
|
||||
|
||||
// Count groups that were actually modified
|
||||
modifiedGroups := 0
|
||||
for _, capture := range captureGroups {
|
||||
if capture.Value != capture.Updated {
|
||||
modifiedGroups++
|
||||
}
|
||||
}
|
||||
logger.Info("%d of %d capture groups identified for modification", modifiedGroups, len(captureGroups))
|
||||
|
||||
for _, capture := range captureGroups {
|
||||
if capture.Value == capture.Updated {
|
||||
logger.Info("Capture group unchanged: %s", LimitString(capture.Value, 50))
|
||||
continue
|
||||
}
|
||||
|
||||
// Log what changed with context
|
||||
logger.Debug("Capture group %s scheduled for modification: %q → %q",
|
||||
capture.Name, capture.Value, capture.Updated)
|
||||
|
||||
// Indices of the group are relative to content
|
||||
// To relate them to match we have to subtract the match start index
|
||||
// replacement = replacement[:groupStart] + newVal + replacement[groupEnd:]
|
||||
commands = append(commands, utils.ReplaceCommand{
|
||||
From: capture.Range[0],
|
||||
To: capture.Range[1],
|
||||
With: capture.Updated,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
commands = append(commands, utils.ReplaceCommand{
|
||||
From: matchIndices[0],
|
||||
To: matchIndices[1],
|
||||
With: replacement,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Total regex processing time: %v", time.Since(startTime))
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func deduplicateGroups(captureGroups []*CaptureGroup) []*CaptureGroup {
|
||||
deduplicatedGroups := make([]*CaptureGroup, 0)
|
||||
for _, group := range captureGroups {
|
||||
overlaps := false
|
||||
logger.Debug("Checking capture group: %s with range %v", group.Name, group.Range)
|
||||
for _, existingGroup := range deduplicatedGroups {
|
||||
logger.Debug("Comparing with existing group: %s with range %v", existingGroup.Name, existingGroup.Range)
|
||||
if group.Range[0] < existingGroup.Range[1] && group.Range[1] > existingGroup.Range[0] {
|
||||
overlaps = true
|
||||
logger.Warning("Detected overlap between capture group '%s' and existing group '%s' in range %v-%v and %v-%v", group.Name, existingGroup.Name, group.Range[0], group.Range[1], existingGroup.Range[0], existingGroup.Range[1])
|
||||
break
|
||||
}
|
||||
}
|
||||
if overlaps {
|
||||
// We CAN just continue despite this fuckup
|
||||
logger.Warning("Overlapping capture group: %s", group.Name)
|
||||
continue
|
||||
}
|
||||
logger.Debug("No overlap detected for capture group: %s. Adding to deduplicated groups.", group.Name)
|
||||
deduplicatedGroups = append(deduplicatedGroups, group)
|
||||
}
|
||||
return deduplicatedGroups
|
||||
}
|
||||
|
||||
// The order of these replaces is important
|
||||
// This one handles !num-s inside of named capture groups
|
||||
// If it were not here our !num in a named capture group would
|
||||
// Expand to another capture group in the capture group
|
||||
// We really only want one (our named) capture group
|
||||
func resolveRegexPlaceholders(pattern string) string {
|
||||
// Handle special pattern modifications
|
||||
if !strings.HasPrefix(pattern, "(?s)") {
|
||||
pattern = "(?s)" + pattern
|
||||
log.Printf("Pattern modified to include (?s): %s", pattern)
|
||||
}
|
||||
|
||||
// The order of these replaces is important
|
||||
// This one handles !num-s inside of named capture groups
|
||||
// If it were not here our !num in a named capture group would
|
||||
// Expand to another capture group in the capture group
|
||||
// We really only want one (our named) capture group
|
||||
namedGroupNum := regexp.MustCompile(`(?:(\?<[^>]+>)(!num))`)
|
||||
pattern = namedGroupNum.ReplaceAllStringFunc(pattern, func(match string) string {
|
||||
parts := namedGroupNum.FindStringSubmatch(match)
|
||||
@@ -94,7 +290,7 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
||||
replacement := `-?\d*\.?\d+`
|
||||
return parts[1] + replacement
|
||||
})
|
||||
pattern = strings.ReplaceAll(pattern, "!num", `"?(-?\d*\.?\d+)"?`)
|
||||
pattern = strings.ReplaceAll(pattern, "!num", `(-?\d*\.?\d+)`)
|
||||
pattern = strings.ReplaceAll(pattern, "!any", `.*?`)
|
||||
repPattern := regexp.MustCompile(`!rep\(([^,]+),\s*(\d+)\)`)
|
||||
// !rep(pattern, count) repeats the pattern n times
|
||||
@@ -109,174 +305,87 @@ func (p *RegexProcessor) ProcessContent(content string, pattern string, luaExpr
|
||||
repetitions, _ := strconv.Atoi(count)
|
||||
return strings.Repeat(repeatedPattern+".*?", repetitions-1) + repeatedPattern
|
||||
})
|
||||
return pattern
|
||||
}
|
||||
|
||||
compiledPattern, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
log.Printf("Error compiling pattern: %v", err)
|
||||
return "", 0, 0, fmt.Errorf("error compiling pattern: %v", err)
|
||||
}
|
||||
log.Printf("Compiled pattern successfully: %s", pattern)
|
||||
|
||||
previous := luaExpr
|
||||
luaExpr = BuildLuaScript(luaExpr)
|
||||
log.Printf("Changing Lua expression from: %s to: %s", previous, luaExpr)
|
||||
|
||||
// Initialize Lua environment
|
||||
modificationCount := 0
|
||||
|
||||
// Process all regex matches
|
||||
result := content
|
||||
indices := compiledPattern.FindAllStringSubmatchIndex(content, -1)
|
||||
log.Printf("Found %d matches in the content", len(indices))
|
||||
|
||||
// We walk backwards because we're replacing something with something else that might be longer
|
||||
// And in the case it is longer than the original all indicces past that change will be fucked up
|
||||
// By going backwards we fuck up all the indices to the end of the file that we don't care about
|
||||
// Because there either aren't any (last match) or they're already modified (subsequent matches)
|
||||
for i := len(indices) - 1; i >= 0; i-- {
|
||||
L, err := NewLuaState()
|
||||
if err != nil {
|
||||
log.Printf("Error creating Lua state: %v", err)
|
||||
return "", 0, 0, fmt.Errorf("error creating Lua state: %v", err)
|
||||
}
|
||||
// Hmm... Maybe we don't want to defer this..
|
||||
// Maybe we want to close them every iteration
|
||||
// We'll leave it as is for now
|
||||
defer L.Close()
|
||||
log.Printf("Lua state created successfully")
|
||||
|
||||
matchIndices := indices[i]
|
||||
log.Printf("Processing match indices: %v", matchIndices)
|
||||
|
||||
// Why we're doing this whole song and dance of indices is to properly handle empty matches
|
||||
// Plus it's a little cleaner to surgically replace our matches
|
||||
// If we were to use string.replace and encountered an empty match there'd be nothing to replace
|
||||
// But using indices an empty match would have its starting and ending indices be the same
|
||||
// So when we're cutting open the array we say 0:7 + modified + 7:end
|
||||
// As if concatenating in the middle of the array
|
||||
// Plus it supports lookarounds
|
||||
match := content[matchIndices[0]:matchIndices[1]]
|
||||
log.Printf("Matched content: %s", match)
|
||||
|
||||
groups := matchIndices[2:]
|
||||
if len(groups) <= 0 {
|
||||
log.Println("No capture groups for lua to chew on")
|
||||
continue
|
||||
}
|
||||
if len(groups)%2 == 1 {
|
||||
log.Println("Odd number of indices of groups, what the fuck?")
|
||||
continue
|
||||
}
|
||||
for _, index := range groups {
|
||||
if index == -1 {
|
||||
// return "", 0, 0, fmt.Errorf("negative indices encountered: %v. This indicates that there was an issue with the match indices, possibly due to an empty match or an unexpected pattern. Please check the regex pattern and input content.", matchIndices)
|
||||
log.Printf("Negative indices encountered: %v. This indicates that there was an issue with the match indices, possibly due to an empty match or an unexpected pattern. This is not an error but it's possibly not what you want.", matchIndices)
|
||||
continue
|
||||
}
|
||||
// ToLua sets capture groups as Lua variables (v1, v2, etc. for numeric values and s1, s2, etc. for strings)
|
||||
func toLua(L *lua.LState, data interface{}) error {
|
||||
captureGroups, ok := data.([]*CaptureGroup)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected []*CaptureGroup for captures, got %T", data)
|
||||
}
|
||||
|
||||
captures := make([]string, 0, len(groups)/2)
|
||||
for j := 0; j < len(groups); j += 2 {
|
||||
if groups[j] == -1 || groups[j+1] == -1 {
|
||||
continue
|
||||
}
|
||||
captures = append(captures, content[groups[j]:groups[j+1]])
|
||||
}
|
||||
log.Printf("Captured groups: %v", captures)
|
||||
|
||||
// We have to use array to preserve order
|
||||
// Very important for the reconstruction step
|
||||
// Because we must overwrite the values in reverse order
|
||||
// See comments a few dozen lines above for more details
|
||||
namedCaptures := make([]NamedCapture, 0, len(groups)/2)
|
||||
groupNames := compiledPattern.SubexpNames()[1:]
|
||||
for i, name := range groupNames {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
if groups[i*2] == -1 || groups[i*2+1] == -1 {
|
||||
continue
|
||||
}
|
||||
namedCaptures = append(namedCaptures, NamedCapture{
|
||||
Name: name,
|
||||
Value: captures[i],
|
||||
Range: [2]int{groups[i*2], groups[i*2+1]},
|
||||
})
|
||||
}
|
||||
|
||||
log.Printf("Named captures: %v", namedCaptures)
|
||||
|
||||
if err := p.ToLua(L, captures); err != nil {
|
||||
log.Printf("Error setting Lua variables: %v", err)
|
||||
continue
|
||||
}
|
||||
log.Println("Lua variables set successfully")
|
||||
|
||||
for _, capture := range namedCaptures {
|
||||
groupindex := 0
|
||||
for _, capture := range captureGroups {
|
||||
if capture.Name == "" {
|
||||
continue
|
||||
// We don't want to change the name of the capture group
|
||||
// Even if it's empty
|
||||
tempName := fmt.Sprintf("%d", groupindex+1)
|
||||
groupindex++
|
||||
|
||||
L.SetGlobal("s"+tempName, lua.LString(capture.Value))
|
||||
|
||||
val, err := strconv.ParseFloat(capture.Value, 64)
|
||||
if err == nil {
|
||||
L.SetGlobal("v"+tempName, lua.LNumber(val))
|
||||
}
|
||||
if val, err := strconv.ParseFloat(capture.Value, 64); err == nil {
|
||||
} else {
|
||||
val, err := strconv.ParseFloat(capture.Value, 64)
|
||||
if err == nil {
|
||||
L.SetGlobal(capture.Name, lua.LNumber(val))
|
||||
} else {
|
||||
L.SetGlobal(capture.Name, lua.LString(capture.Value))
|
||||
}
|
||||
}
|
||||
|
||||
if err := L.DoString(luaExpr); err != nil {
|
||||
log.Printf("Error executing Lua code %s for group %s: %v", luaExpr, captures, err)
|
||||
continue
|
||||
}
|
||||
log.Println("Lua code executed successfully")
|
||||
|
||||
// Get modifications from Lua
|
||||
modResult, err := p.FromLua(L)
|
||||
if err != nil {
|
||||
log.Printf("Error getting modifications: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply modifications to the matched text
|
||||
modsMap, ok := modResult.(map[int]string)
|
||||
if !ok || len(modsMap) == 0 {
|
||||
log.Println("No modifications to apply")
|
||||
continue
|
||||
}
|
||||
|
||||
replacement := ""
|
||||
replacementVar := L.GetGlobal("replacement")
|
||||
if replacementVar.Type() != lua.LTNil {
|
||||
replacement = replacementVar.String()
|
||||
}
|
||||
if replacement == "" {
|
||||
// Apply the modifications to the original match
|
||||
replacement = match
|
||||
for i := len(modsMap) - 1; i >= 0; i-- {
|
||||
newVal := modsMap[i]
|
||||
log.Printf("Applying modification: %s", newVal)
|
||||
// Indices of the group are relative to content
|
||||
// To relate them to match we have to subtract the match start index
|
||||
groupStart := groups[i*2] - matchIndices[0]
|
||||
groupEnd := groups[i*2+1] - matchIndices[0]
|
||||
replacement = replacement[:groupStart] + newVal + replacement[groupEnd:]
|
||||
}
|
||||
|
||||
for i := len(namedCaptures) - 1; i >= 0; i-- {
|
||||
capture := namedCaptures[i]
|
||||
if capture.Name == "" {
|
||||
continue
|
||||
}
|
||||
groupStart := capture.Range[0] - matchIndices[0]
|
||||
groupEnd := capture.Range[1] - matchIndices[0]
|
||||
luaValue := L.GetGlobal(capture.Name).String()
|
||||
replacement = replacement[:groupStart] + luaValue + replacement[groupEnd:]
|
||||
}
|
||||
}
|
||||
modificationCount++
|
||||
result = result[:matchIndices[0]] + replacement + result[matchIndices[1]:]
|
||||
log.Printf("Modification count updated: %d", modificationCount)
|
||||
}
|
||||
|
||||
log.Printf("Process completed with %d modifications", modificationCount)
|
||||
return result, modificationCount, len(indices), nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// FromLua implements the Processor interface for RegexProcessor
|
||||
func fromLua(L *lua.LState, captureGroups []*CaptureGroup) ([]*CaptureGroup, error) {
|
||||
captureIndex := 0
|
||||
for _, capture := range captureGroups {
|
||||
if capture.Name == "" {
|
||||
capture.Name = fmt.Sprintf("%d", captureIndex+1)
|
||||
|
||||
vVarName := fmt.Sprintf("v%s", capture.Name)
|
||||
sVarName := fmt.Sprintf("s%s", capture.Name)
|
||||
captureIndex++
|
||||
|
||||
vLuaVal := L.GetGlobal(vVarName)
|
||||
sLuaVal := L.GetGlobal(sVarName)
|
||||
|
||||
if sLuaVal.Type() == lua.LTString {
|
||||
capture.Updated = sLuaVal.String()
|
||||
}
|
||||
// Numbers have priority
|
||||
if vLuaVal.Type() == lua.LTNumber {
|
||||
capture.Updated = vLuaVal.String()
|
||||
}
|
||||
} else {
|
||||
// Easy shit
|
||||
capture.Updated = L.GetGlobal(capture.Name).String()
|
||||
}
|
||||
}
|
||||
|
||||
return captureGroups, nil
|
||||
}
|
||||
|
||||
// estimatePatternComplexity gives a rough estimate of regex pattern complexity
|
||||
// This can help identify potentially problematic patterns
|
||||
func estimatePatternComplexity(pattern string) int {
|
||||
complexity := len(pattern)
|
||||
|
||||
// Add complexity for potentially expensive operations
|
||||
complexity += strings.Count(pattern, ".*") * 10 // Greedy wildcard
|
||||
complexity += strings.Count(pattern, ".*?") * 5 // Non-greedy wildcard
|
||||
complexity += strings.Count(pattern, "[^") * 3 // Negated character class
|
||||
complexity += strings.Count(pattern, "\\b") * 2 // Word boundary
|
||||
complexity += strings.Count(pattern, "(") * 2 // Capture groups
|
||||
complexity += strings.Count(pattern, "(?:") * 1 // Non-capture groups
|
||||
complexity += strings.Count(pattern, "\\1") * 3 // Backreferences
|
||||
complexity += strings.Count(pattern, "{") * 2 // Counted repetition
|
||||
|
||||
return complexity
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
27
processor/test_helper.go
Normal file
27
processor/test_helper.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Only modify logger in test mode
|
||||
// This checks if we're running under 'go test'
|
||||
if os.Getenv("GO_TESTING") == "1" || os.Getenv("TESTING") == "1" {
|
||||
// Initialize logger with ERROR level for tests
|
||||
// to minimize noise in test output
|
||||
logger.Init(logger.LevelError)
|
||||
|
||||
// Optionally redirect logger output to discard
|
||||
// This prevents logger output from interfering with test output
|
||||
disableTestLogs := os.Getenv("ENABLE_TEST_LOGS") != "1"
|
||||
if disableTestLogs {
|
||||
// Create a new logger that writes to nowhere
|
||||
silentLogger := logger.New(io.Discard, "", 0)
|
||||
logger.Default = silentLogger
|
||||
}
|
||||
}
|
||||
}
|
412
processor/xml.go
412
processor/xml.go
@@ -1,412 +0,0 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"modify/processor/xpath"
|
||||
"strings"
|
||||
|
||||
"github.com/antchfx/xmlquery"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// XMLProcessor implements the Processor interface for XML documents
|
||||
type XMLProcessor struct{}
|
||||
|
||||
// ProcessContent implements the Processor interface for XMLProcessor
|
||||
func (p *XMLProcessor) ProcessContent(content string, path string, luaExpr string) (string, int, int, error) {
|
||||
// Parse XML document
|
||||
// We can't really use encoding/xml here because it requires a pre defined struct
|
||||
// And we HAVE TO parse dynamic unknown XML
|
||||
doc, err := xmlquery.Parse(strings.NewReader(content))
|
||||
if err != nil {
|
||||
return content, 0, 0, fmt.Errorf("error parsing XML: %v", err)
|
||||
}
|
||||
|
||||
// Find nodes matching the XPath pattern
|
||||
nodes, err := xpath.Get(doc, path)
|
||||
if err != nil {
|
||||
return content, 0, 0, fmt.Errorf("error executing XPath: %v", err)
|
||||
}
|
||||
|
||||
matchCount := len(nodes)
|
||||
if matchCount == 0 {
|
||||
return content, 0, 0, nil
|
||||
}
|
||||
|
||||
// Apply modifications to each node
|
||||
modCount := 0
|
||||
for _, node := range nodes {
|
||||
L, err := NewLuaState()
|
||||
if err != nil {
|
||||
return content, 0, 0, fmt.Errorf("error creating Lua state: %v", err)
|
||||
}
|
||||
defer L.Close()
|
||||
|
||||
err = p.ToLua(L, node)
|
||||
if err != nil {
|
||||
return content, modCount, matchCount, fmt.Errorf("error converting to Lua: %v", err)
|
||||
}
|
||||
|
||||
err = L.DoString(BuildLuaScript(luaExpr))
|
||||
if err != nil {
|
||||
return content, modCount, matchCount, fmt.Errorf("error executing Lua: %v", err)
|
||||
}
|
||||
|
||||
result, err := p.FromLua(L)
|
||||
if err != nil {
|
||||
return content, modCount, matchCount, fmt.Errorf("error getting result from Lua: %v", err)
|
||||
}
|
||||
log.Printf("%#v", result)
|
||||
|
||||
modified := false
|
||||
modified = L.GetGlobal("modified").String() == "true"
|
||||
if !modified {
|
||||
log.Printf("No changes made to node at path: %s", node.Data)
|
||||
continue
|
||||
}
|
||||
|
||||
// Apply modification based on the result
|
||||
if updatedValue, ok := result.(string); ok {
|
||||
// If the result is a simple string, update the node value directly
|
||||
xpath.Set(doc, path, updatedValue)
|
||||
} else if nodeData, ok := result.(map[string]interface{}); ok {
|
||||
// If the result is a map, apply more complex updates
|
||||
updateNodeFromMap(node, nodeData)
|
||||
}
|
||||
|
||||
modCount++
|
||||
}
|
||||
|
||||
// Serialize the modified XML document to string
|
||||
if doc.FirstChild != nil && doc.FirstChild.Type == xmlquery.DeclarationNode {
|
||||
// If we have an XML declaration, start with it
|
||||
declaration := doc.FirstChild.OutputXML(true)
|
||||
// Remove the firstChild (declaration) before serializing the rest of the document
|
||||
doc.FirstChild = doc.FirstChild.NextSibling
|
||||
return ConvertToNamedEntities(declaration + doc.OutputXML(true)), modCount, matchCount, nil
|
||||
}
|
||||
|
||||
// Convert numeric entities to named entities for better readability
|
||||
return ConvertToNamedEntities(doc.OutputXML(true)), modCount, matchCount, nil
|
||||
}
|
||||
|
||||
func (p *XMLProcessor) ToLua(L *lua.LState, data interface{}) error {
|
||||
table, err := p.ToLuaTable(L, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
L.SetGlobal("v", table)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToLua converts XML node values to Lua variables
|
||||
func (p *XMLProcessor) ToLuaTable(L *lua.LState, data interface{}) (lua.LValue, error) {
|
||||
// Check if data is an xmlquery.Node
|
||||
node, ok := data.(*xmlquery.Node)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected xmlquery.Node, got %T", data)
|
||||
}
|
||||
|
||||
// Create a simple table with essential data
|
||||
table := L.NewTable()
|
||||
|
||||
// For element nodes, just provide basic info
|
||||
L.SetField(table, "type", lua.LString(nodeTypeToString(node.Type)))
|
||||
L.SetField(table, "name", lua.LString(node.Data))
|
||||
L.SetField(table, "value", lua.LString(node.InnerText()))
|
||||
|
||||
// Add children if any
|
||||
children := L.NewTable()
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
childTable, err := p.ToLuaTable(L, child)
|
||||
if err == nil {
|
||||
children.Append(childTable)
|
||||
}
|
||||
}
|
||||
L.SetField(table, "children", children)
|
||||
|
||||
attrs := L.NewTable()
|
||||
if len(node.Attr) > 0 {
|
||||
for _, attr := range node.Attr {
|
||||
L.SetField(attrs, attr.Name.Local, lua.LString(attr.Value))
|
||||
}
|
||||
}
|
||||
L.SetField(table, "attr", attrs)
|
||||
|
||||
return table, nil
|
||||
}
|
||||
|
||||
// FromLua gets modified values from Lua
|
||||
func (p *XMLProcessor) FromLua(L *lua.LState) (interface{}, error) {
|
||||
luaValue := L.GetGlobal("v")
|
||||
|
||||
// Handle string values directly
|
||||
if luaValue.Type() == lua.LTString {
|
||||
return luaValue.String(), nil
|
||||
}
|
||||
|
||||
// Handle tables (for attributes and more complex updates)
|
||||
if luaValue.Type() == lua.LTTable {
|
||||
return luaTableToMap(L, luaValue.(*lua.LTable)), nil
|
||||
}
|
||||
|
||||
return luaValue.String(), nil
|
||||
}
|
||||
|
||||
// Simple helper to convert a Lua table to a Go map
|
||||
func luaTableToMap(L *lua.LState, table *lua.LTable) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
|
||||
table.ForEach(func(k, v lua.LValue) {
|
||||
if k.Type() == lua.LTString {
|
||||
key := k.String()
|
||||
|
||||
if v.Type() == lua.LTTable {
|
||||
result[key] = luaTableToMap(L, v.(*lua.LTable))
|
||||
} else {
|
||||
result[key] = v.String()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Simple helper to convert node type to string
|
||||
func nodeTypeToString(nodeType xmlquery.NodeType) string {
|
||||
switch nodeType {
|
||||
case xmlquery.ElementNode:
|
||||
return "element"
|
||||
case xmlquery.TextNode:
|
||||
return "text"
|
||||
case xmlquery.AttributeNode:
|
||||
return "attribute"
|
||||
default:
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to update an XML node from a map
|
||||
func updateNodeFromMap(node *xmlquery.Node, data map[string]interface{}) {
|
||||
// Update node value if present
|
||||
if value, ok := data["value"]; ok {
|
||||
if strValue, ok := value.(string); ok {
|
||||
// For element nodes, replace text content
|
||||
if node.Type == xmlquery.ElementNode {
|
||||
// Find the first text child if it exists
|
||||
var textNode *xmlquery.Node
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
if child.Type == xmlquery.TextNode {
|
||||
textNode = child
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if textNode != nil {
|
||||
// Update existing text node
|
||||
textNode.Data = strValue
|
||||
} else {
|
||||
// Create new text node
|
||||
newText := &xmlquery.Node{
|
||||
Type: xmlquery.TextNode,
|
||||
Data: strValue,
|
||||
Parent: node,
|
||||
}
|
||||
|
||||
// Insert at beginning of children
|
||||
if node.FirstChild != nil {
|
||||
newText.NextSibling = node.FirstChild
|
||||
node.FirstChild.PrevSibling = newText
|
||||
node.FirstChild = newText
|
||||
} else {
|
||||
node.FirstChild = newText
|
||||
node.LastChild = newText
|
||||
}
|
||||
}
|
||||
} else if node.Type == xmlquery.TextNode {
|
||||
// Directly update text node
|
||||
node.Data = strValue
|
||||
} else if node.Type == xmlquery.AttributeNode {
|
||||
// Update attribute value
|
||||
if node.Parent != nil {
|
||||
for i, attr := range node.Parent.Attr {
|
||||
if attr.Name.Local == node.Data {
|
||||
node.Parent.Attr[i].Value = strValue
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update attributes if present
|
||||
if attrs, ok := data["attr"].(map[string]interface{}); ok && node.Type == xmlquery.ElementNode {
|
||||
for name, value := range attrs {
|
||||
if strValue, ok := value.(string); ok {
|
||||
// Look for existing attribute
|
||||
found := false
|
||||
for i, attr := range node.Attr {
|
||||
if attr.Name.Local == name {
|
||||
node.Attr[i].Value = strValue
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Add new attribute if not found
|
||||
if !found {
|
||||
node.Attr = append(node.Attr, xmlquery.Attr{
|
||||
Name: struct {
|
||||
Space, Local string
|
||||
}{Local: name},
|
||||
Value: strValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get a string representation of node type
|
||||
func nodeTypeName(nodeType xmlquery.NodeType) string {
|
||||
switch nodeType {
|
||||
case xmlquery.ElementNode:
|
||||
return "element"
|
||||
case xmlquery.TextNode:
|
||||
return "text"
|
||||
case xmlquery.AttributeNode:
|
||||
return "attribute"
|
||||
case xmlquery.CommentNode:
|
||||
return "comment"
|
||||
case xmlquery.DeclarationNode:
|
||||
return "declaration"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertToNamedEntities replaces numeric XML entities with their named counterparts
|
||||
func ConvertToNamedEntities(xml string) string {
|
||||
// Basic XML entities
|
||||
replacements := map[string]string{
|
||||
// Basic XML entities
|
||||
""": """, // double quote
|
||||
"'": "'", // single quote
|
||||
"<": "<", // less than
|
||||
">": ">", // greater than
|
||||
"&": "&", // ampersand
|
||||
|
||||
// Common symbols
|
||||
" ": " ", // non-breaking space
|
||||
"©": "©", // copyright
|
||||
"®": "®", // registered trademark
|
||||
"€": "€", // euro
|
||||
"£": "£", // pound
|
||||
"¥": "¥", // yen
|
||||
"¢": "¢", // cent
|
||||
"§": "§", // section
|
||||
"™": "™", // trademark
|
||||
"♠": "♠", // spade
|
||||
"♣": "♣", // club
|
||||
"♥": "♥", // heart
|
||||
"♦": "♦", // diamond
|
||||
|
||||
// Special characters
|
||||
"¡": "¡", // inverted exclamation
|
||||
"¿": "¿", // inverted question
|
||||
"«": "«", // left angle quotes
|
||||
"»": "»", // right angle quotes
|
||||
"·": "·", // middle dot
|
||||
"•": "•", // bullet
|
||||
"…": "…", // horizontal ellipsis
|
||||
"′": "′", // prime
|
||||
"″": "″", // double prime
|
||||
"‾": "‾", // overline
|
||||
"⁄": "⁄", // fraction slash
|
||||
|
||||
// Math symbols
|
||||
"±": "±", // plus-minus
|
||||
"×": "×", // multiplication
|
||||
"÷": "÷", // division
|
||||
"∞": "∞", // infinity
|
||||
"≈": "≈", // almost equal
|
||||
"≠": "≠", // not equal
|
||||
"≤": "≤", // less than or equal
|
||||
"≥": "≥", // greater than or equal
|
||||
"∑": "∑", // summation
|
||||
"√": "√", // square root
|
||||
"∫": "∫", // integral
|
||||
|
||||
// Accented characters
|
||||
"À": "À", // A grave
|
||||
"Á": "Á", // A acute
|
||||
"Â": "Â", // A circumflex
|
||||
"Ã": "Ã", // A tilde
|
||||
"Ä": "Ä", // A umlaut
|
||||
"Å": "Å", // A ring
|
||||
"Æ": "Æ", // AE ligature
|
||||
"Ç": "Ç", // C cedilla
|
||||
"È": "È", // E grave
|
||||
"É": "É", // E acute
|
||||
"Ê": "Ê", // E circumflex
|
||||
"Ë": "Ë", // E umlaut
|
||||
"Ì": "Ì", // I grave
|
||||
"Í": "Í", // I acute
|
||||
"Î": "Î", // I circumflex
|
||||
"Ï": "Ï", // I umlaut
|
||||
"Ð": "Ð", // Eth
|
||||
"Ñ": "Ñ", // N tilde
|
||||
"Ò": "Ò", // O grave
|
||||
"Ó": "Ó", // O acute
|
||||
"Ô": "Ô", // O circumflex
|
||||
"Õ": "Õ", // O tilde
|
||||
"Ö": "Ö", // O umlaut
|
||||
"Ø": "Ø", // O slash
|
||||
"Ù": "Ù", // U grave
|
||||
"Ú": "Ú", // U acute
|
||||
"Û": "Û", // U circumflex
|
||||
"Ü": "Ü", // U umlaut
|
||||
"Ý": "Ý", // Y acute
|
||||
"Þ": "Þ", // Thorn
|
||||
"ß": "ß", // Sharp s
|
||||
"à": "à", // a grave
|
||||
"á": "á", // a acute
|
||||
"â": "â", // a circumflex
|
||||
"ã": "ã", // a tilde
|
||||
"ä": "ä", // a umlaut
|
||||
"å": "å", // a ring
|
||||
"æ": "æ", // ae ligature
|
||||
"ç": "ç", // c cedilla
|
||||
"è": "è", // e grave
|
||||
"é": "é", // e acute
|
||||
"ê": "ê", // e circumflex
|
||||
"ë": "ë", // e umlaut
|
||||
"ì": "ì", // i grave
|
||||
"í": "í", // i acute
|
||||
"î": "î", // i circumflex
|
||||
"ï": "ï", // i umlaut
|
||||
"ð": "ð", // eth
|
||||
"ñ": "ñ", // n tilde
|
||||
"ò": "ò", // o grave
|
||||
"ó": "ó", // o acute
|
||||
"ô": "ô", // o circumflex
|
||||
"õ": "õ", // o tilde
|
||||
"ö": "ö", // o umlaut
|
||||
"ø": "ø", // o slash
|
||||
"ù": "ù", // u grave
|
||||
"ú": "ú", // u acute
|
||||
"û": "û", // u circumflex
|
||||
"ü": "ü", // u umlaut
|
||||
"ý": "ý", // y acute
|
||||
"þ": "þ", // thorn
|
||||
"ÿ": "ÿ", // y umlaut
|
||||
}
|
||||
|
||||
result := xml
|
||||
for numeric, named := range replacements {
|
||||
result = strings.ReplaceAll(result, numeric, named)
|
||||
}
|
||||
return result
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
// The package is now using github.com/antchfx/xmlquery for XPath parsing.
|
||||
// The parsing functionality tests have been removed since we're now
|
||||
// delegating XPath parsing to the xmlquery library.
|
||||
package xpath
|
@@ -1,4 +0,0 @@
|
||||
// The package is now using github.com/antchfx/xmlquery for XPath parsing.
|
||||
// The parsing functionality tests have been removed since we're now
|
||||
// delegating XPath parsing to the xmlquery library.
|
||||
package xpath
|
@@ -1,133 +0,0 @@
|
||||
package xpath
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/antchfx/xmlquery"
|
||||
)
|
||||
|
||||
// Get retrieves nodes from XML data using an XPath expression
|
||||
func Get(node *xmlquery.Node, path string) ([]*xmlquery.Node, error) {
|
||||
if node == nil {
|
||||
return nil, errors.New("nil node provided")
|
||||
}
|
||||
|
||||
// Execute xpath query directly
|
||||
nodes, err := xmlquery.QueryAll(node, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute XPath query: %v", err)
|
||||
}
|
||||
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
// Set updates a single node in the XML data using an XPath expression
|
||||
func Set(node *xmlquery.Node, path string, value interface{}) error {
|
||||
if node == nil {
|
||||
return errors.New("nil node provided")
|
||||
}
|
||||
|
||||
// Find the node to update
|
||||
nodes, err := xmlquery.QueryAll(node, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute XPath query: %v", err)
|
||||
}
|
||||
|
||||
if len(nodes) == 0 {
|
||||
return fmt.Errorf("no nodes found for path: %s", path)
|
||||
}
|
||||
|
||||
// Update the first matching node
|
||||
updateNodeValue(nodes[0], value)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetAll updates all nodes that match the XPath expression
|
||||
func SetAll(node *xmlquery.Node, path string, value interface{}) error {
|
||||
if node == nil {
|
||||
return errors.New("nil node provided")
|
||||
}
|
||||
|
||||
// Find all nodes to update
|
||||
nodes, err := xmlquery.QueryAll(node, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute XPath query: %v", err)
|
||||
}
|
||||
|
||||
if len(nodes) == 0 {
|
||||
return fmt.Errorf("no nodes found for path: %s", path)
|
||||
}
|
||||
|
||||
// Update all matching nodes
|
||||
for _, matchNode := range nodes {
|
||||
updateNodeValue(matchNode, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to update a node's value
|
||||
func updateNodeValue(node *xmlquery.Node, value interface{}) {
|
||||
strValue := fmt.Sprintf("%v", value)
|
||||
|
||||
// Handle different node types
|
||||
switch node.Type {
|
||||
case xmlquery.AttributeNode:
|
||||
// For attribute nodes, update the attribute value
|
||||
parent := node.Parent
|
||||
if parent != nil {
|
||||
for i, attr := range parent.Attr {
|
||||
if attr.Name.Local == node.Data {
|
||||
parent.Attr[i].Value = strValue
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
case xmlquery.TextNode:
|
||||
// For text nodes, update the text content
|
||||
node.Data = strValue
|
||||
case xmlquery.ElementNode:
|
||||
// For element nodes, clear existing text children and add a new text node
|
||||
// First, remove all existing text children
|
||||
var nonTextChildren []*xmlquery.Node
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
if child.Type != xmlquery.TextNode {
|
||||
nonTextChildren = append(nonTextChildren, child)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all children
|
||||
node.FirstChild = nil
|
||||
node.LastChild = nil
|
||||
|
||||
// Add a new text node
|
||||
textNode := &xmlquery.Node{
|
||||
Type: xmlquery.TextNode,
|
||||
Data: strValue,
|
||||
Parent: node,
|
||||
}
|
||||
|
||||
// Set the text node as the first child
|
||||
node.FirstChild = textNode
|
||||
node.LastChild = textNode
|
||||
|
||||
// Add back non-text children
|
||||
for _, child := range nonTextChildren {
|
||||
child.Parent = node
|
||||
|
||||
// If this is the first child being added back
|
||||
if node.FirstChild == textNode && node.LastChild == textNode {
|
||||
node.FirstChild.NextSibling = child
|
||||
child.PrevSibling = node.FirstChild
|
||||
node.LastChild = child
|
||||
} else {
|
||||
// Add to the end of the chain
|
||||
node.LastChild.NextSibling = child
|
||||
child.PrevSibling = node.LastChild
|
||||
node.LastChild = child
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,474 +0,0 @@
|
||||
package xpath
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/antchfx/xmlquery"
|
||||
)
|
||||
|
||||
// Parse test XML data once at the beginning for use in multiple tests
|
||||
func parseTestXML(t *testing.T, xmlData string) *xmlquery.Node {
|
||||
doc, err := xmlquery.Parse(strings.NewReader(xmlData))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse test XML: %v", err)
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
// XML test data as a string for our tests
|
||||
var testXML = `
|
||||
<store>
|
||||
<book category="fiction">
|
||||
<title lang="en">The Fellowship of the Ring</title>
|
||||
<author>J.R.R. Tolkien</author>
|
||||
<year>1954</year>
|
||||
<price>22.99</price>
|
||||
</book>
|
||||
<book category="fiction">
|
||||
<title lang="en">The Two Towers</title>
|
||||
<author>J.R.R. Tolkien</author>
|
||||
<year>1954</year>
|
||||
<price>23.45</price>
|
||||
</book>
|
||||
<book category="technical">
|
||||
<title lang="en">Learning XML</title>
|
||||
<author>Erik T. Ray</author>
|
||||
<year>2003</year>
|
||||
<price>39.95</price>
|
||||
</book>
|
||||
<bicycle>
|
||||
<color>red</color>
|
||||
<price>199.95</price>
|
||||
</bicycle>
|
||||
</store>
|
||||
`
|
||||
|
||||
func TestEvaluator(t *testing.T) {
|
||||
// Parse the test XML data once for all test cases
|
||||
doc := parseTestXML(t, testXML)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
error bool
|
||||
}{
|
||||
{
|
||||
name: "simple_element_access",
|
||||
path: "/store/bicycle/color",
|
||||
},
|
||||
{
|
||||
name: "recursive_element_access",
|
||||
path: "//price",
|
||||
},
|
||||
{
|
||||
name: "wildcard_element_access",
|
||||
path: "/store/book/*",
|
||||
},
|
||||
{
|
||||
name: "attribute_exists_predicate",
|
||||
path: "//title[@lang]",
|
||||
},
|
||||
{
|
||||
name: "attribute_equals_predicate",
|
||||
path: "//title[@lang='en']",
|
||||
},
|
||||
{
|
||||
name: "value_comparison_predicate",
|
||||
path: "/store/book[price>35.00]/title",
|
||||
error: true,
|
||||
},
|
||||
{
|
||||
name: "last_predicate",
|
||||
path: "/store/book[last()]/title",
|
||||
error: true,
|
||||
},
|
||||
{
|
||||
name: "last_minus_predicate",
|
||||
path: "/store/book[last()-1]/title",
|
||||
error: true,
|
||||
},
|
||||
{
|
||||
name: "position_predicate",
|
||||
path: "/store/book[position()<3]/title",
|
||||
error: true,
|
||||
},
|
||||
{
|
||||
name: "invalid_index",
|
||||
path: "/store/book[10]/title",
|
||||
error: true,
|
||||
},
|
||||
{
|
||||
name: "nonexistent_element",
|
||||
path: "/store/nonexistent",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := Get(doc, tt.path)
|
||||
|
||||
// Handle expected errors
|
||||
if tt.error {
|
||||
if err == nil && len(result) == 0 {
|
||||
// If we expected an error but got empty results instead, that's okay
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
// If we got an error as expected, that's okay
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
// If we didn't expect an error but got one, that's a test failure
|
||||
t.Errorf("Get(%q) returned unexpected error: %v", tt.path, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Special cases where we don't care about exact matches
|
||||
switch tt.name {
|
||||
case "wildcard_element_access":
|
||||
// Just check that we got some elements
|
||||
if len(result) == 0 {
|
||||
t.Errorf("Expected multiple elements for wildcard, got none")
|
||||
}
|
||||
return
|
||||
case "attribute_exists_predicate", "attribute_equals_predicate":
|
||||
// Just check that we got some titles
|
||||
if len(result) == 0 {
|
||||
t.Errorf("Expected titles with lang attribute, got none")
|
||||
}
|
||||
// Ensure all are title elements
|
||||
for _, node := range result {
|
||||
if node.Data != "title" {
|
||||
t.Errorf("Expected title elements, got: %s", node.Data)
|
||||
}
|
||||
}
|
||||
return
|
||||
case "nonexistent_element":
|
||||
// Just check that we got empty results
|
||||
if len(result) != 0 {
|
||||
t.Errorf("Expected empty results for nonexistent element, got %d items", len(result))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// For other cases, just verify we got results
|
||||
if len(result) == 0 {
|
||||
t.Errorf("Expected results for path %s, got none", tt.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
t.Run("nil_node", func(t *testing.T) {
|
||||
result, err := Get(nil, "/store/book")
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for nil node")
|
||||
return
|
||||
}
|
||||
if len(result) > 0 {
|
||||
t.Errorf("Expected empty result, got %v", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid_xml", func(t *testing.T) {
|
||||
invalidXML, err := xmlquery.Parse(strings.NewReader("<invalid>xml"))
|
||||
if err != nil {
|
||||
// If parsing fails, that's expected
|
||||
return
|
||||
}
|
||||
|
||||
_, err = Get(invalidXML, "/store")
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid XML structure")
|
||||
}
|
||||
})
|
||||
|
||||
// For these tests with the simple XML, we expect just one result
|
||||
simpleXML := `<root><book><title lang="en">Test</title></book></root>`
|
||||
doc := parseTestXML(t, simpleXML)
|
||||
|
||||
t.Run("current_node", func(t *testing.T) {
|
||||
result, err := Get(doc, "/root/book/.")
|
||||
if err != nil {
|
||||
t.Errorf("Get() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
if len(result) > 1 {
|
||||
t.Errorf("Expected at most 1 result, got %d", len(result))
|
||||
}
|
||||
if len(result) > 0 {
|
||||
// Verify it's the book node
|
||||
if result[0].Data != "book" {
|
||||
t.Errorf("Expected book node, got %v", result[0].Data)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("attributes", func(t *testing.T) {
|
||||
result, err := Get(doc, "/root/book/title/@lang")
|
||||
if err != nil {
|
||||
t.Errorf("Get() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
if len(result) != 1 || result[0].InnerText() != "en" {
|
||||
t.Errorf("Expected 'en', got %v", result[0].InnerText())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetWithPaths(t *testing.T) {
|
||||
// Use a simplified, well-formed XML document
|
||||
simpleXML := `<store>
|
||||
<book category="fiction">
|
||||
<title lang="en">The Book Title</title>
|
||||
<author>Author Name</author>
|
||||
<price>19.99</price>
|
||||
</book>
|
||||
<bicycle>
|
||||
<color>red</color>
|
||||
<price>199.95</price>
|
||||
</bicycle>
|
||||
</store>`
|
||||
|
||||
// Parse the XML for testing
|
||||
doc := parseTestXML(t, simpleXML)
|
||||
|
||||
// Debug: Print the test XML
|
||||
t.Logf("Test XML:\n%s", simpleXML)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
expectedValue string
|
||||
}{
|
||||
{
|
||||
name: "simple_element_access",
|
||||
path: "/store/bicycle/color",
|
||||
expectedValue: "red",
|
||||
},
|
||||
{
|
||||
name: "attribute_access",
|
||||
path: "/store/book/title/@lang",
|
||||
expectedValue: "en",
|
||||
},
|
||||
{
|
||||
name: "recursive_with_attribute",
|
||||
path: "//title[@lang='en']",
|
||||
expectedValue: "The Book Title",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Debug: Print the path we're looking for
|
||||
t.Logf("Looking for path: %s", tt.path)
|
||||
|
||||
result, err := Get(doc, tt.path)
|
||||
if err != nil {
|
||||
t.Errorf("Get(%q) returned error: %v", tt.path, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Debug: Print the results
|
||||
t.Logf("Got %d results", len(result))
|
||||
for i, r := range result {
|
||||
t.Logf("Result %d: Node=%s, Value=%v", i, r.Data, r.InnerText())
|
||||
}
|
||||
|
||||
// Check that we got results
|
||||
if len(result) == 0 {
|
||||
t.Errorf("Get(%q) returned no results", tt.path)
|
||||
return
|
||||
}
|
||||
|
||||
// For attribute access test, do more specific checks
|
||||
if tt.name == "attribute_access" {
|
||||
// Check the first result's value matches expected
|
||||
if result[0].InnerText() != tt.expectedValue {
|
||||
t.Errorf("Attribute value: got %v, expected %s", result[0].InnerText(), tt.expectedValue)
|
||||
}
|
||||
}
|
||||
|
||||
// For simple element access, check the text content
|
||||
if tt.name == "simple_element_access" {
|
||||
if text := result[0].InnerText(); text != tt.expectedValue {
|
||||
t.Errorf("Element text: got %s, expected %s", text, tt.expectedValue)
|
||||
}
|
||||
}
|
||||
|
||||
// For recursive with attribute test, check title elements with lang="en"
|
||||
if tt.name == "recursive_with_attribute" {
|
||||
for _, node := range result {
|
||||
// Check the node is a title
|
||||
if node.Data != "title" {
|
||||
t.Errorf("Expected title element, got %s", node.Data)
|
||||
}
|
||||
|
||||
// Check text content
|
||||
if text := node.InnerText(); text != tt.expectedValue {
|
||||
t.Errorf("Text content: got %s, expected %s", text, tt.expectedValue)
|
||||
}
|
||||
|
||||
// Check attributes - find the lang attribute
|
||||
hasLang := false
|
||||
for _, attr := range node.Attr {
|
||||
if attr.Name.Local == "lang" && attr.Value == "en" {
|
||||
hasLang = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasLang {
|
||||
t.Errorf("Expected lang=\"en\" attribute, but it was not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
t.Run("simple element", func(t *testing.T) {
|
||||
xmlData := `<root><name>John</name></root>`
|
||||
doc := parseTestXML(t, xmlData)
|
||||
|
||||
err := Set(doc, "/root/name", "Jane")
|
||||
if err != nil {
|
||||
t.Errorf("Set() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the change
|
||||
result, err := Get(doc, "/root/name")
|
||||
if err != nil {
|
||||
t.Errorf("Get() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
if len(result) != 1 {
|
||||
t.Errorf("Expected 1 result, got %d", len(result))
|
||||
return
|
||||
}
|
||||
|
||||
// Check text content
|
||||
if text := result[0].InnerText(); text != "Jane" {
|
||||
t.Errorf("Expected text 'Jane', got '%s'", text)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("attribute", func(t *testing.T) {
|
||||
xmlData := `<root><element id="123"></element></root>`
|
||||
doc := parseTestXML(t, xmlData)
|
||||
|
||||
err := Set(doc, "/root/element/@id", "456")
|
||||
if err != nil {
|
||||
t.Errorf("Set() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the change
|
||||
result, err := Get(doc, "/root/element/@id")
|
||||
if err != nil {
|
||||
t.Errorf("Get() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
if len(result) != 1 {
|
||||
t.Errorf("Expected 1 result, got %d", len(result))
|
||||
return
|
||||
}
|
||||
|
||||
// For attributes, check the inner text
|
||||
if text := result[0].InnerText(); text != "456" {
|
||||
t.Errorf("Expected attribute value '456', got '%s'", text)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("indexed element", func(t *testing.T) {
|
||||
xmlData := `<root><items><item>first</item><item>second</item></items></root>`
|
||||
doc := parseTestXML(t, xmlData)
|
||||
|
||||
err := Set(doc, "/root/items/item[1]", "changed")
|
||||
if err != nil {
|
||||
t.Errorf("Set() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the change using XPath that specifically targets the first item
|
||||
result, err := Get(doc, "/root/items/item[1]")
|
||||
if err != nil {
|
||||
t.Errorf("Get() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we have results
|
||||
if len(result) == 0 {
|
||||
t.Errorf("Expected at least one result for /root/items/item[1]")
|
||||
return
|
||||
}
|
||||
|
||||
// Check text content
|
||||
if text := result[0].InnerText(); text != "changed" {
|
||||
t.Errorf("Expected text 'changed', got '%s'", text)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetAll(t *testing.T) {
|
||||
t.Run("multiple elements", func(t *testing.T) {
|
||||
xmlData := `<root><items><item>first</item><item>second</item></items></root>`
|
||||
doc := parseTestXML(t, xmlData)
|
||||
|
||||
err := SetAll(doc, "//item", "changed")
|
||||
if err != nil {
|
||||
t.Errorf("SetAll() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify all items are changed
|
||||
result, err := Get(doc, "//item")
|
||||
if err != nil {
|
||||
t.Errorf("Get() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
if len(result) != 2 {
|
||||
t.Errorf("Expected 2 results, got %d", len(result))
|
||||
return
|
||||
}
|
||||
|
||||
// Check each node
|
||||
for i, node := range result {
|
||||
if text := node.InnerText(); text != "changed" {
|
||||
t.Errorf("Item %d: expected text 'changed', got '%s'", i, text)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("attributes", func(t *testing.T) {
|
||||
xmlData := `<root><item id="1"/><item id="2"/></root>`
|
||||
doc := parseTestXML(t, xmlData)
|
||||
|
||||
err := SetAll(doc, "//item/@id", "new")
|
||||
if err != nil {
|
||||
t.Errorf("SetAll() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify all attributes are changed
|
||||
result, err := Get(doc, "//item/@id")
|
||||
if err != nil {
|
||||
t.Errorf("Get() returned error: %v", err)
|
||||
return
|
||||
}
|
||||
if len(result) != 2 {
|
||||
t.Errorf("Expected 2 results, got %d", len(result))
|
||||
return
|
||||
}
|
||||
|
||||
// For attributes, check inner text
|
||||
for i, node := range result {
|
||||
if text := node.InnerText(); text != "new" {
|
||||
t.Errorf("Attribute %d: expected value 'new', got '%s'", i, text)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
137
regression/regression_test.go
Normal file
137
regression/regression_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package regression
|
||||
|
||||
import (
|
||||
"cook/processor"
|
||||
"cook/utils"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func ApiAdaptor(content string, regex string, lua string) (string, int, int, error) {
|
||||
command := utils.ModifyCommand{
|
||||
Regex: regex,
|
||||
Lua: lua,
|
||||
LogLevel: "TRACE",
|
||||
}
|
||||
|
||||
commands, err := processor.ProcessRegex(content, command, "test")
|
||||
if err != nil {
|
||||
return "", 0, 0, err
|
||||
}
|
||||
|
||||
result, modifications := utils.ExecuteModifications(commands, content)
|
||||
return result, modifications, len(commands), nil
|
||||
}
|
||||
|
||||
func TestTalentsMechanicOutOfRange(t *testing.T) {
|
||||
given := `<Talent identifier="quickfixer">
|
||||
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="5,2" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.quickfixer">
|
||||
<Replace tag="[amount]" value="20" color="gui.green"/>
|
||||
<Replace tag="[duration]" value="10" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.repairmechanicaldevicestwiceasfast"/>
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveStat stattype="MechanicalRepairSpeed" value="1"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
|
||||
<Conditions>
|
||||
<AbilityConditionItem tags="fabricator,door,engine,oxygengenerator,pump,turretammosource,deconstructor,medicalfabricator,ductblock"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityApplyStatusEffects>
|
||||
<StatusEffects>
|
||||
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true">
|
||||
<Affliction identifier="quickfixer" amount="10.0"/>
|
||||
</StatusEffect>
|
||||
</StatusEffects>
|
||||
</CharacterAbilityApplyStatusEffects>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>`
|
||||
|
||||
actual := `<Talent identifier="quickfixer">
|
||||
<Icon texture="Content/UI/TalentsIcons2.png" sheetindex="5,2" sheetelementsize="128,128"/>
|
||||
<Description tag="talentdescription.quickfixer">
|
||||
<Replace tag="[amount]" value="30" color="gui.green"/>
|
||||
<Replace tag="[duration]" value="20" color="gui.green"/>
|
||||
</Description>
|
||||
<Description tag="talentdescription.repairmechanicaldevicestwiceasfast"/>
|
||||
<AbilityGroupEffect abilityeffecttype="None">
|
||||
<Abilities>
|
||||
<CharacterAbilityGiveStat stattype="MechanicalRepairSpeed" value="2"/>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
<AbilityGroupEffect abilityeffecttype="OnRepairComplete">
|
||||
<Conditions>
|
||||
<AbilityConditionItem tags="fabricator,door,engine,oxygengenerator,pump,turretammosource,deconstructor,medicalfabricator,ductblock"/>
|
||||
</Conditions>
|
||||
<Abilities>
|
||||
<CharacterAbilityApplyStatusEffects>
|
||||
<StatusEffects>
|
||||
<StatusEffect type="OnAbility" target="Character" disabledeltatime="true">
|
||||
<Affliction identifier="quickfixer" amount="20"/>
|
||||
</StatusEffect>
|
||||
</StatusEffects>
|
||||
</CharacterAbilityApplyStatusEffects>
|
||||
</Abilities>
|
||||
</AbilityGroupEffect>
|
||||
</Talent>`
|
||||
|
||||
result, mods, matches, err := ApiAdaptor(given, `<Talent identifier="quickfixer">!anyvalue="(?<movementspeed>!num)"!anyvalue="(?<duration>!num)"!anyvalue="(?<repairspeed>!num)"!anyamount="(?<durationv>!num)"`, "movementspeed=round(movementspeed*1.5, 2) duration=round(duration*2, 2) repairspeed=round(repairspeed*2, 2) durationv=duration")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Error processing content: %v", err)
|
||||
}
|
||||
|
||||
if matches != 4 {
|
||||
t.Errorf("Expected 4 matches, got %d", matches)
|
||||
}
|
||||
|
||||
if mods != 4 {
|
||||
t.Errorf("Expected 4 modifications, got %d", mods)
|
||||
}
|
||||
|
||||
if result != actual {
|
||||
t.Errorf("expected %s, got %s", actual, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexExplosions_ShouldNotPanic(t *testing.T) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("Error getting current working directory: %v", err)
|
||||
}
|
||||
|
||||
given, err := os.ReadFile(filepath.Join(cwd, "..", "testfiles", "OutpostItems.xml"))
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading file: %v", err)
|
||||
}
|
||||
|
||||
expected, err := os.ReadFile(filepath.Join(cwd, "..", "testfiles", "OutpostItemsExpected.xml"))
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading file: %v", err)
|
||||
}
|
||||
|
||||
result, _, _, err := ApiAdaptor(string(given), `(?-s)LightComponent!anyrange="(!num)"`, "*4")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Error processing content: %v", err)
|
||||
}
|
||||
|
||||
// We don't really care how many god damn matches there are as long as the result is correct
|
||||
// if matches != 45 {
|
||||
// t.Errorf("Expected 45 match, got %d", matches)
|
||||
// }
|
||||
//
|
||||
// if mods != 45 {
|
||||
// t.Errorf("Expected 45 modification, got %d", mods)
|
||||
// }
|
||||
|
||||
if string(result) != string(expected) {
|
||||
t.Errorf("expected %s, got %s", expected, result)
|
||||
}
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
<config>
|
||||
<item>
|
||||
<value>75</value>
|
||||
<multiplier>2</multiplier>
|
||||
<divider>4</divider>
|
||||
</item>
|
||||
<item>
|
||||
<value>150</value>
|
||||
<multiplier>3</multiplier>
|
||||
<divider>2</divider>
|
||||
</item>
|
||||
</config>
|
@@ -1,37 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<testdata>
|
||||
<!-- Numeric values -->
|
||||
<item>
|
||||
<id>1</id>
|
||||
<value>200</value>
|
||||
<price>24.99</price>
|
||||
<quantity>5</quantity>
|
||||
</item>
|
||||
|
||||
<!-- Text values -->
|
||||
<item>
|
||||
<id>2</id>
|
||||
<name>Test Product</name>
|
||||
<description>This is a test product description</description>
|
||||
<category>Test</category>
|
||||
</item>
|
||||
|
||||
<!-- Mixed content -->
|
||||
<item>
|
||||
<id>3</id>
|
||||
<name>Mixed Product</name>
|
||||
<price>19.99</price>
|
||||
<code>PRD-123</code>
|
||||
<tags>sale,discount,new</tags>
|
||||
</item>
|
||||
|
||||
<!-- Empty and special values -->
|
||||
<item>
|
||||
<id>4</id>
|
||||
<value></value>
|
||||
<specialChars>Hello & World < > " '</specialChars>
|
||||
<multiline>Line 1
|
||||
Line 2
|
||||
Line 3</multiline>
|
||||
</item>
|
||||
</testdata>
|
1252
testfiles/OutpostItems.xml
Normal file
1252
testfiles/OutpostItems.xml
Normal file
File diff suppressed because it is too large
Load Diff
1252
testfiles/OutpostItemsExpected.xml
Normal file
1252
testfiles/OutpostItemsExpected.xml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
<config><item><value>100</value></item></config>
|
120
utils/db.go
Normal file
120
utils/db.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"git.site.quack-lab.dev/dave/cylogger"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DB interface {
|
||||
DB() *gorm.DB
|
||||
Raw(sql string, args ...any) *gorm.DB
|
||||
SaveFile(filePath string, fileData []byte) error
|
||||
GetFile(filePath string) ([]byte, error)
|
||||
GetAllFiles() ([]FileSnapshot, error)
|
||||
RemoveAllFiles() error
|
||||
}
|
||||
|
||||
type FileSnapshot struct {
|
||||
Date time.Time `gorm:"primaryKey"`
|
||||
FilePath string `gorm:"primaryKey"`
|
||||
FileData []byte `gorm:"type:blob"`
|
||||
}
|
||||
|
||||
type DBWrapper struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
var db *DBWrapper
|
||||
|
||||
func GetDB() (DB, error) {
|
||||
var err error
|
||||
|
||||
dbFile := filepath.Join("data.sqlite")
|
||||
db, err := gorm.Open(sqlite.Open(dbFile), &gorm.Config{
|
||||
// SkipDefaultTransaction: true,
|
||||
PrepareStmt: true,
|
||||
// Logger: gormlogger.Default.LogMode(gormlogger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.AutoMigrate(&FileSnapshot{})
|
||||
|
||||
return &DBWrapper{db: db}, nil
|
||||
}
|
||||
|
||||
// Just a wrapper
|
||||
func (db *DBWrapper) Raw(sql string, args ...any) *gorm.DB {
|
||||
return db.db.Raw(sql, args...)
|
||||
}
|
||||
|
||||
func (db *DBWrapper) DB() *gorm.DB {
|
||||
return db.db
|
||||
}
|
||||
|
||||
func (db *DBWrapper) FileExists(filePath string) (bool, error) {
|
||||
var count int64
|
||||
err := db.db.Model(&FileSnapshot{}).Where("file_path = ?", filePath).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (db *DBWrapper) SaveFile(filePath string, fileData []byte) error {
|
||||
log := cylogger.Default.WithPrefix(fmt.Sprintf("SaveFile: %q", filePath))
|
||||
exists, err := db.FileExists(filePath)
|
||||
if err != nil {
|
||||
log.Error("Error checking if file exists: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Debug("File exists: %t", exists)
|
||||
// Nothing to do, file already exists
|
||||
if exists {
|
||||
log.Debug("File already exists, skipping save")
|
||||
return nil
|
||||
}
|
||||
log.Debug("Saving file to database")
|
||||
return db.db.Create(&FileSnapshot{
|
||||
Date: time.Now(),
|
||||
FilePath: filePath,
|
||||
FileData: fileData,
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (db *DBWrapper) GetFile(filePath string) ([]byte, error) {
|
||||
log := cylogger.Default.WithPrefix(fmt.Sprintf("GetFile: %q", filePath))
|
||||
log.Debug("Getting file from database")
|
||||
var fileSnapshot FileSnapshot
|
||||
err := db.db.Model(&FileSnapshot{}).Where("file_path = ?", filePath).First(&fileSnapshot).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug("File found in database")
|
||||
return fileSnapshot.FileData, nil
|
||||
}
|
||||
|
||||
func (db *DBWrapper) GetAllFiles() ([]FileSnapshot, error) {
|
||||
log := cylogger.Default.WithPrefix("GetAllFiles")
|
||||
log.Debug("Getting all files from database")
|
||||
var fileSnapshots []FileSnapshot
|
||||
err := db.db.Model(&FileSnapshot{}).Find(&fileSnapshots).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debug("Found %d files in database", len(fileSnapshots))
|
||||
return fileSnapshots, nil
|
||||
}
|
||||
|
||||
func (db *DBWrapper) RemoveAllFiles() error {
|
||||
log := cylogger.Default.WithPrefix("RemoveAllFiles")
|
||||
log.Debug("Removing all files from database")
|
||||
err := db.db.Exec("DELETE FROM file_snapshots").Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("All files removed from database")
|
||||
return nil
|
||||
}
|
96
utils/file.go
Normal file
96
utils/file.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.site.quack-lab.dev/dave/cylogger"
|
||||
)
|
||||
|
||||
func CleanPath(path string) string {
|
||||
log := cylogger.Default.WithPrefix(fmt.Sprintf("CleanPath: %q", path))
|
||||
log.Trace("Start")
|
||||
path = filepath.Clean(path)
|
||||
path = strings.ReplaceAll(path, "\\", "/")
|
||||
log.Trace("Done: %q", path)
|
||||
return path
|
||||
}
|
||||
|
||||
func ToAbs(path string) string {
|
||||
log := cylogger.Default.WithPrefix(fmt.Sprintf("ToAbs: %q", path))
|
||||
log.Trace("Start")
|
||||
if filepath.IsAbs(path) {
|
||||
log.Trace("Path is already absolute: %q", path)
|
||||
return CleanPath(path)
|
||||
}
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Error("Error getting cwd: %v", err)
|
||||
return CleanPath(path)
|
||||
}
|
||||
log.Trace("Cwd: %q", cwd)
|
||||
return CleanPath(filepath.Join(cwd, path))
|
||||
}
|
||||
|
||||
func ResetWhereNecessary(associations map[string]FileCommandAssociation, db DB) error {
|
||||
log := cylogger.Default.WithPrefix("ResetWhereNecessary")
|
||||
log.Debug("Start")
|
||||
dirtyFiles := make(map[string]struct{})
|
||||
for _, association := range associations {
|
||||
for _, command := range association.Commands {
|
||||
log.Debug("Checking command %q for file %q", command.Name, association.File)
|
||||
if command.Reset {
|
||||
log.Debug("Command %q requires reset for file %q", command.Name, association.File)
|
||||
dirtyFiles[association.File] = struct{}{}
|
||||
}
|
||||
}
|
||||
for _, command := range association.IsolateCommands {
|
||||
log.Debug("Checking isolate command %q for file %q", command.Name, association.File)
|
||||
if command.Reset {
|
||||
log.Debug("Isolate command %q requires reset for file %q", command.Name, association.File)
|
||||
dirtyFiles[association.File] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Debug("Dirty files: %v", dirtyFiles)
|
||||
for file := range dirtyFiles {
|
||||
log.Debug("Resetting file %q", file)
|
||||
fileData, err := db.GetFile(file)
|
||||
if err != nil {
|
||||
log.Warning("Failed to get file %q: %v", file, err)
|
||||
continue
|
||||
}
|
||||
log.Debug("Writing file %q to disk", file)
|
||||
err = os.WriteFile(file, fileData, 0644)
|
||||
if err != nil {
|
||||
log.Warning("Failed to write file %q: %v", file, err)
|
||||
continue
|
||||
}
|
||||
log.Debug("File %q written to disk", file)
|
||||
}
|
||||
log.Debug("Done")
|
||||
return nil
|
||||
}
|
||||
|
||||
func ResetAllFiles(db DB) error {
|
||||
log := cylogger.Default.WithPrefix("ResetAllFiles")
|
||||
log.Debug("Start")
|
||||
fileSnapshots, err := db.GetAllFiles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debug("Found %d files in database", len(fileSnapshots))
|
||||
for _, fileSnapshot := range fileSnapshots {
|
||||
log.Debug("Resetting file %q", fileSnapshot.FilePath)
|
||||
err = os.WriteFile(fileSnapshot.FilePath, fileSnapshot.FileData, 0644)
|
||||
if err != nil {
|
||||
log.Warning("Failed to write file %q: %v", fileSnapshot.FilePath, err)
|
||||
continue
|
||||
}
|
||||
log.Debug("File %q written to disk", fileSnapshot.FilePath)
|
||||
}
|
||||
log.Debug("Done")
|
||||
return nil
|
||||
}
|
10
utils/flags.go
Normal file
10
utils/flags.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"flag"
|
||||
)
|
||||
|
||||
var (
|
||||
ParallelFiles = flag.Int("P", 100, "Number of files to process in parallel")
|
||||
Filter = flag.String("f", "", "Filter commands before running them")
|
||||
)
|
260
utils/modifycommand.go
Normal file
260
utils/modifycommand.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type ModifyCommand struct {
|
||||
Name string `yaml:"name"`
|
||||
Regex string `yaml:"regex"`
|
||||
Lua string `yaml:"lua"`
|
||||
Files []string `yaml:"files"`
|
||||
Reset bool `yaml:"reset"`
|
||||
LogLevel string `yaml:"loglevel"`
|
||||
Isolate bool `yaml:"isolate"`
|
||||
NoDedup bool `yaml:"nodedup"`
|
||||
Disabled bool `yaml:"disable"`
|
||||
}
|
||||
type CookFile []ModifyCommand
|
||||
|
||||
func (c *ModifyCommand) Validate() error {
|
||||
if c.Regex == "" {
|
||||
return fmt.Errorf("pattern is required")
|
||||
}
|
||||
if c.Lua == "" {
|
||||
return fmt.Errorf("lua expression is required")
|
||||
}
|
||||
if len(c.Files) == 0 {
|
||||
return fmt.Errorf("at least one file is required")
|
||||
}
|
||||
if c.LogLevel == "" {
|
||||
c.LogLevel = "INFO"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ehh.. Not much better... Guess this wasn't the big deal
|
||||
var matchesMemoTable map[string]bool = make(map[string]bool)
|
||||
|
||||
func Matches(path string, glob string) (bool, error) {
|
||||
key := fmt.Sprintf("%s:%s", path, glob)
|
||||
if matches, ok := matchesMemoTable[key]; ok {
|
||||
logger.Debug("Found match for file %q and glob %q in memo table", path, glob)
|
||||
return matches, nil
|
||||
}
|
||||
matches, err := doublestar.Match(glob, path)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to match glob %s with file %s: %w", glob, path, err)
|
||||
}
|
||||
matchesMemoTable[key] = matches
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
func SplitPattern(pattern string) (string, string) {
|
||||
static, pattern := doublestar.SplitPattern(pattern)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
if static == "" {
|
||||
static = cwd
|
||||
}
|
||||
if !filepath.IsAbs(static) {
|
||||
static = filepath.Join(cwd, static)
|
||||
static = filepath.Clean(static)
|
||||
}
|
||||
static = strings.ReplaceAll(static, "\\", "/")
|
||||
return static, pattern
|
||||
}
|
||||
|
||||
type FileCommandAssociation struct {
|
||||
File string
|
||||
IsolateCommands []ModifyCommand
|
||||
Commands []ModifyCommand
|
||||
}
|
||||
|
||||
func AssociateFilesWithCommands(files []string, commands []ModifyCommand) (map[string]FileCommandAssociation, error) {
|
||||
associationCount := 0
|
||||
fileCommands := make(map[string]FileCommandAssociation)
|
||||
|
||||
for _, file := range files {
|
||||
file = strings.ReplaceAll(file, "\\", "/")
|
||||
fileCommands[file] = FileCommandAssociation{
|
||||
File: file,
|
||||
IsolateCommands: []ModifyCommand{},
|
||||
Commands: []ModifyCommand{},
|
||||
}
|
||||
for _, command := range commands {
|
||||
for _, glob := range command.Files {
|
||||
static, pattern := SplitPattern(glob)
|
||||
patternFile := strings.Replace(file, static+`/`, "", 1)
|
||||
matches, err := Matches(patternFile, pattern)
|
||||
if err != nil {
|
||||
logger.Trace("Failed to match glob %s with file %s: %v", glob, file, err)
|
||||
continue
|
||||
}
|
||||
if matches {
|
||||
logger.Debug("Found match for file %q and command %q", file, command.Regex)
|
||||
association := fileCommands[file]
|
||||
|
||||
if command.Isolate {
|
||||
association.IsolateCommands = append(association.IsolateCommands, command)
|
||||
} else {
|
||||
association.Commands = append(association.Commands, command)
|
||||
}
|
||||
fileCommands[file] = association
|
||||
associationCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.Debug("Found %d commands for file %q", len(fileCommands[file].Commands), file)
|
||||
if len(fileCommands[file].Commands) == 0 {
|
||||
logger.Info("No commands found for file %q", file)
|
||||
}
|
||||
if len(fileCommands[file].IsolateCommands) > 0 {
|
||||
logger.Info("Found %d isolate commands for file %q", len(fileCommands[file].IsolateCommands), file)
|
||||
}
|
||||
}
|
||||
logger.Info("Found %d associations between %d files and %d commands", associationCount, len(files), len(commands))
|
||||
return fileCommands, nil
|
||||
}
|
||||
|
||||
func AggregateGlobs(commands []ModifyCommand) map[string]struct{} {
|
||||
logger.Info("Aggregating globs for %d commands", len(commands))
|
||||
globs := make(map[string]struct{})
|
||||
for _, command := range commands {
|
||||
for _, glob := range command.Files {
|
||||
glob = strings.Replace(glob, "~", os.Getenv("HOME"), 1)
|
||||
glob = strings.ReplaceAll(glob, "\\", "/")
|
||||
globs[glob] = struct{}{}
|
||||
}
|
||||
}
|
||||
logger.Info("Found %d unique globs", len(globs))
|
||||
return globs
|
||||
}
|
||||
|
||||
func ExpandGLobs(patterns map[string]struct{}) ([]string, error) {
|
||||
var files []string
|
||||
filesMap := make(map[string]bool)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get current working directory: %w", err)
|
||||
}
|
||||
|
||||
logger.Debug("Expanding patterns from directory: %s", cwd)
|
||||
for pattern := range patterns {
|
||||
logger.Trace("Processing pattern: %s", pattern)
|
||||
static, pattern := SplitPattern(pattern)
|
||||
matches, _ := doublestar.Glob(os.DirFS(static), pattern)
|
||||
logger.Debug("Found %d matches for pattern %s", len(matches), pattern)
|
||||
for _, m := range matches {
|
||||
m = filepath.Join(static, m)
|
||||
info, err := os.Stat(m)
|
||||
if err != nil {
|
||||
logger.Warning("Error getting file info for %s: %v", m, err)
|
||||
continue
|
||||
}
|
||||
if !info.IsDir() && !filesMap[m] {
|
||||
logger.Trace("Adding file to process list: %s", m)
|
||||
filesMap[m], files = true, append(files, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(files) > 0 {
|
||||
logger.Debug("Found %d files to process: %v", len(files), files)
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func LoadCommands(args []string) ([]ModifyCommand, error) {
|
||||
commands := []ModifyCommand{}
|
||||
|
||||
logger.Info("Loading commands from cook files: %s", args)
|
||||
for _, arg := range args {
|
||||
newcommands, err := LoadCommandsFromCookFiles(arg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load commands from cook files: %w", err)
|
||||
}
|
||||
logger.Info("Successfully loaded %d commands from cook files", len(newcommands))
|
||||
for _, cmd := range newcommands {
|
||||
if cmd.Disabled {
|
||||
logger.Info("Skipping disabled command: %s", cmd.Name)
|
||||
continue
|
||||
}
|
||||
commands = append(commands, cmd)
|
||||
}
|
||||
logger.Info("Now total commands: %d", len(commands))
|
||||
}
|
||||
|
||||
logger.Info("Loaded %d commands from all cook file", len(commands))
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func LoadCommandsFromCookFiles(pattern string) ([]ModifyCommand, error) {
|
||||
static, pattern := SplitPattern(pattern)
|
||||
commands := []ModifyCommand{}
|
||||
cookFiles, err := doublestar.Glob(os.DirFS(static), pattern)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to glob cook files: %w", err)
|
||||
}
|
||||
|
||||
for _, cookFile := range cookFiles {
|
||||
cookFile = filepath.Join(static, cookFile)
|
||||
cookFile = filepath.Clean(cookFile)
|
||||
cookFile = strings.ReplaceAll(cookFile, "\\", "/")
|
||||
logger.Info("Loading commands from cook file: %s", cookFile)
|
||||
|
||||
cookFileData, err := os.ReadFile(cookFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read cook file: %w", err)
|
||||
}
|
||||
newcommands, err := LoadCommandsFromCookFile(cookFileData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load commands from cook file: %w", err)
|
||||
}
|
||||
commands = append(commands, newcommands...)
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
func LoadCommandsFromCookFile(cookFileData []byte) ([]ModifyCommand, error) {
|
||||
commands := []ModifyCommand{}
|
||||
err := yaml.Unmarshal(cookFileData, &commands)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal cook file: %w", err)
|
||||
}
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
// CountGlobsBeforeDedup counts the total number of glob patterns across all commands before deduplication
|
||||
func CountGlobsBeforeDedup(commands []ModifyCommand) int {
|
||||
count := 0
|
||||
for _, cmd := range commands {
|
||||
count += len(cmd.Files)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func FilterCommands(commands []ModifyCommand, filter string) []ModifyCommand {
|
||||
filteredCommands := []ModifyCommand{}
|
||||
filters := strings.Split(filter, ",")
|
||||
for _, cmd := range commands {
|
||||
for _, filter := range filters {
|
||||
if strings.Contains(cmd.Name, filter) {
|
||||
filteredCommands = append(filteredCommands, cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredCommands
|
||||
}
|
1000
utils/modifycommand_test.go
Normal file
1000
utils/modifycommand_test.go
Normal file
File diff suppressed because it is too large
Load Diff
58
utils/replacecommand.go
Normal file
58
utils/replacecommand.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
logger "git.site.quack-lab.dev/dave/cylogger"
|
||||
)
|
||||
|
||||
type ReplaceCommand struct {
|
||||
From int
|
||||
To int
|
||||
With string
|
||||
}
|
||||
|
||||
func ExecuteModifications(modifications []ReplaceCommand, fileData string) (string, int) {
|
||||
var err error
|
||||
|
||||
sort.Slice(modifications, func(i, j int) bool {
|
||||
return modifications[i].From > modifications[j].From
|
||||
})
|
||||
logger.Trace("Preparing to apply %d replacement commands in reverse order", len(modifications))
|
||||
|
||||
executed := 0
|
||||
for _, modification := range modifications {
|
||||
fileData, err = modification.Execute(fileData)
|
||||
if err != nil {
|
||||
logger.Error("Failed to execute replacement: %v", err)
|
||||
continue
|
||||
}
|
||||
executed++
|
||||
}
|
||||
logger.Info("Successfully applied %d text replacements", executed)
|
||||
return fileData, executed
|
||||
}
|
||||
|
||||
func (m *ReplaceCommand) Execute(fileDataStr string) (string, error) {
|
||||
err := m.Validate(len(fileDataStr))
|
||||
if err != nil {
|
||||
return fileDataStr, fmt.Errorf("failed to validate modification: %v", err)
|
||||
}
|
||||
|
||||
logger.Trace("Replace pos %d-%d with %q", m.From, m.To, m.With)
|
||||
return fileDataStr[:m.From] + m.With + fileDataStr[m.To:], nil
|
||||
}
|
||||
|
||||
func (m *ReplaceCommand) Validate(maxsize int) error {
|
||||
if m.To < m.From {
|
||||
return fmt.Errorf("command to is less than from: %v", m)
|
||||
}
|
||||
if m.From > maxsize || m.To > maxsize {
|
||||
return fmt.Errorf("command from or to is greater than replacement length: %v", m)
|
||||
}
|
||||
if m.From < 0 || m.To < 0 {
|
||||
return fmt.Errorf("command from or to is less than 0: %v", m)
|
||||
}
|
||||
return nil
|
||||
}
|
504
utils/replacecommand_test.go
Normal file
504
utils/replacecommand_test.go
Normal file
@@ -0,0 +1,504 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestReplaceCommandExecute(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
command ReplaceCommand
|
||||
expected string
|
||||
shouldError bool
|
||||
}{
|
||||
{
|
||||
name: "Simple replacement",
|
||||
input: "This is a test string",
|
||||
command: ReplaceCommand{From: 5, To: 7, With: "was"},
|
||||
expected: "This was a test string",
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "Replace at beginning",
|
||||
input: "Hello world",
|
||||
command: ReplaceCommand{From: 0, To: 5, With: "Hi"},
|
||||
expected: "Hi world",
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "Replace at end",
|
||||
input: "Hello world",
|
||||
command: ReplaceCommand{From: 6, To: 11, With: "everyone"},
|
||||
expected: "Hello everyone",
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "Replace entire string",
|
||||
input: "Hello world",
|
||||
command: ReplaceCommand{From: 0, To: 11, With: "Goodbye!"},
|
||||
expected: "Goodbye!",
|
||||
shouldError: false,
|
||||
},
|
||||
{
|
||||
name: "Error: From > To",
|
||||
input: "Test string",
|
||||
command: ReplaceCommand{From: 7, To: 5, With: "fail"},
|
||||
expected: "Test string",
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
name: "Error: From > string length",
|
||||
input: "Test",
|
||||
command: ReplaceCommand{From: 10, To: 12, With: "fail"},
|
||||
expected: "Test",
|
||||
shouldError: true,
|
||||
},
|
||||
{
|
||||
name: "Error: To > string length",
|
||||
input: "Test",
|
||||
command: ReplaceCommand{From: 2, To: 10, With: "fail"},
|
||||
expected: "Test",
|
||||
shouldError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := tc.command.Execute(tc.input)
|
||||
|
||||
if tc.shouldError {
|
||||
if err == nil {
|
||||
t.Errorf("Expected an error for command %+v but got none", tc.command)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if result != tc.expected {
|
||||
t.Errorf("Expected %q, got %q", tc.expected, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteModifications(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
modifications []ReplaceCommand
|
||||
expected string
|
||||
expectedCount int
|
||||
}{
|
||||
{
|
||||
name: "Single modification",
|
||||
input: "Hello world",
|
||||
modifications: []ReplaceCommand{
|
||||
{From: 0, To: 5, With: "Hi"},
|
||||
},
|
||||
expected: "Hi world",
|
||||
expectedCount: 1,
|
||||
},
|
||||
{
|
||||
name: "Multiple modifications",
|
||||
input: "This is a test string",
|
||||
modifications: []ReplaceCommand{
|
||||
{From: 0, To: 4, With: "That"},
|
||||
{From: 8, To: 14, With: "sample"},
|
||||
},
|
||||
expected: "That is sample string",
|
||||
expectedCount: 2,
|
||||
},
|
||||
{
|
||||
name: "Overlapping modifications",
|
||||
input: "ABCDEF",
|
||||
modifications: []ReplaceCommand{
|
||||
{From: 0, To: 3, With: "123"}, // ABC -> 123
|
||||
{From: 2, To: 5, With: "xyz"}, // CDE -> xyz
|
||||
},
|
||||
// The actual behavior with the current implementation
|
||||
expected: "123yzF",
|
||||
expectedCount: 2,
|
||||
},
|
||||
{
|
||||
name: "Sequential modifications",
|
||||
input: "Hello world",
|
||||
modifications: []ReplaceCommand{
|
||||
{From: 0, To: 5, With: "Hi"},
|
||||
{From: 5, To: 6, With: ""}, // Remove the space
|
||||
{From: 6, To: 11, With: "everyone"},
|
||||
},
|
||||
expected: "Hieveryone",
|
||||
expectedCount: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Make a copy of the modifications to avoid modifying the test case
|
||||
mods := make([]ReplaceCommand, len(tc.modifications))
|
||||
copy(mods, tc.modifications)
|
||||
|
||||
result, count := ExecuteModifications(mods, tc.input)
|
||||
|
||||
if count != tc.expectedCount {
|
||||
t.Errorf("Expected %d modifications, got %d", tc.expectedCount, count)
|
||||
}
|
||||
|
||||
if result != tc.expected {
|
||||
t.Errorf("Expected %q, got %q", tc.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReverseOrderExecution(t *testing.T) {
|
||||
// This test verifies the current behavior of modification application
|
||||
input := "Original text with multiple sections"
|
||||
|
||||
// Modifications in specific positions
|
||||
modifications := []ReplaceCommand{
|
||||
{From: 0, To: 8, With: "Modified"}, // Original -> Modified
|
||||
{From: 9, To: 13, With: "document"}, // text -> document
|
||||
{From: 14, To: 22, With: "without"}, // with -> without
|
||||
{From: 23, To: 31, With: "any"}, // multiple -> any
|
||||
}
|
||||
|
||||
// The actual current behavior of our implementation
|
||||
expected := "Modified document withouttanytions"
|
||||
|
||||
result, count := ExecuteModifications(modifications, input)
|
||||
|
||||
if count != 4 {
|
||||
t.Errorf("Expected 4 modifications, got %d", count)
|
||||
}
|
||||
|
||||
if result != expected {
|
||||
t.Errorf("Expected %q, got %q", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
// Replace text in the middle of a string with new content
|
||||
func TestReplaceCommandExecute_ReplacesTextInMiddle(t *testing.T) {
|
||||
// Arrange
|
||||
cmd := &ReplaceCommand{
|
||||
From: 6,
|
||||
To: 11,
|
||||
With: "replaced",
|
||||
}
|
||||
|
||||
fileContent := "Hello world, how are you?"
|
||||
|
||||
// Act
|
||||
result, err := cmd.Execute(fileContent)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Hello replaced, how are you?", result)
|
||||
}
|
||||
|
||||
// Replace with empty string (deletion)
|
||||
func TestReplaceCommandExecute_DeletesText(t *testing.T) {
|
||||
// Arrange
|
||||
cmd := &ReplaceCommand{
|
||||
From: 6,
|
||||
To: 11,
|
||||
With: "",
|
||||
}
|
||||
|
||||
fileContent := "Hello world, how are you?"
|
||||
|
||||
// Act
|
||||
result, err := cmd.Execute(fileContent)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Hello , how are you?", result)
|
||||
}
|
||||
|
||||
// Replace with longer string than original segment
|
||||
func TestReplaceCommandExecute_WithLongerString(t *testing.T) {
|
||||
// Arrange
|
||||
cmd := &ReplaceCommand{
|
||||
From: 6,
|
||||
To: 11,
|
||||
With: "longerreplacement",
|
||||
}
|
||||
|
||||
fileContent := "Hello world, how are you?"
|
||||
|
||||
// Act
|
||||
result, err := cmd.Execute(fileContent)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Hello longerreplacement, how are you?", result)
|
||||
}
|
||||
|
||||
// From and To values are the same (zero-length replacement)
|
||||
func TestReplaceCommandExecute_ZeroLengthReplacement(t *testing.T) {
|
||||
// Arrange
|
||||
cmd := &ReplaceCommand{
|
||||
From: 5,
|
||||
To: 5,
|
||||
With: "inserted",
|
||||
}
|
||||
|
||||
fileContent := "Hello world"
|
||||
|
||||
// Act
|
||||
result, err := cmd.Execute(fileContent)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Helloinserted world", result)
|
||||
}
|
||||
|
||||
// From value is greater than To value
|
||||
func TestReplaceCommandExecute_FromGreaterThanTo(t *testing.T) {
|
||||
// Arrange
|
||||
cmd := &ReplaceCommand{
|
||||
From: 10,
|
||||
To: 5,
|
||||
With: "replaced",
|
||||
}
|
||||
|
||||
fileContent := "Hello world, how are you?"
|
||||
|
||||
// Act
|
||||
result, err := cmd.Execute(fileContent)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Hello world, how are you?", result)
|
||||
}
|
||||
|
||||
// From or To values exceed string length
|
||||
func TestReplaceCommandExecute_FromOrToExceedsLength(t *testing.T) {
|
||||
// Arrange
|
||||
cmd := &ReplaceCommand{
|
||||
From: 5,
|
||||
To: 50, // Exceeds the length of the fileContent
|
||||
With: "replaced",
|
||||
}
|
||||
|
||||
fileContent := "Hello world"
|
||||
|
||||
// Act
|
||||
result, err := cmd.Execute(fileContent)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Hello world", result)
|
||||
}
|
||||
|
||||
// From or To values are negative
|
||||
func TestReplaceCommandExecute_NegativeFromOrTo(t *testing.T) {
|
||||
// Arrange
|
||||
cmd := &ReplaceCommand{
|
||||
From: -1,
|
||||
To: 10,
|
||||
With: "replaced",
|
||||
}
|
||||
|
||||
fileContent := "Hello world, how are you?"
|
||||
|
||||
// Act
|
||||
result, err := cmd.Execute(fileContent)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "Hello world, how are you?", result)
|
||||
}
|
||||
|
||||
// Modifications are applied in reverse order (from highest to lowest 'From' value)
|
||||
func TestExecuteModificationsAppliesInReverseOrder(t *testing.T) {
|
||||
// Setup test data
|
||||
fileData := "This is a test string for replacements"
|
||||
|
||||
modifications := []ReplaceCommand{
|
||||
{From: 0, To: 4, With: "That"},
|
||||
{From: 10, To: 14, With: "sample"},
|
||||
{From: 26, To: 38, With: "modifications"},
|
||||
}
|
||||
|
||||
// Execute the function
|
||||
result, executed := ExecuteModifications(modifications, fileData)
|
||||
|
||||
// Verify results
|
||||
expectedResult := "That is a sample string for modifications"
|
||||
if result != expectedResult {
|
||||
t.Errorf("Expected result to be %q, but got %q", expectedResult, result)
|
||||
}
|
||||
|
||||
if executed != 3 {
|
||||
t.Errorf("Expected 3 modifications to be executed, but got %d", executed)
|
||||
}
|
||||
}
|
||||
|
||||
// One or more modifications fail but others succeed
|
||||
func TestExecuteModificationsWithPartialFailures(t *testing.T) {
|
||||
// Setup test data
|
||||
fileData := "This is a test string for replacements"
|
||||
|
||||
// Create a custom ReplaceCommand implementation that will fail
|
||||
failingCommand := ReplaceCommand{
|
||||
From: 15,
|
||||
To: 10, // Invalid range (To < From) to cause failure
|
||||
With: "will fail",
|
||||
}
|
||||
|
||||
// Valid commands
|
||||
validCommand1 := ReplaceCommand{
|
||||
From: 0,
|
||||
To: 4,
|
||||
With: "That",
|
||||
}
|
||||
|
||||
validCommand2 := ReplaceCommand{
|
||||
From: 26,
|
||||
To: 38,
|
||||
With: "modifications",
|
||||
}
|
||||
|
||||
modifications := []ReplaceCommand{failingCommand, validCommand1, validCommand2}
|
||||
|
||||
// Execute the function
|
||||
result, executed := ExecuteModifications(modifications, fileData)
|
||||
|
||||
// Verify results
|
||||
expectedResult := "That is a test string for modifications"
|
||||
if result != expectedResult {
|
||||
t.Errorf("Expected result to be %q, but got %q", expectedResult, result)
|
||||
}
|
||||
|
||||
// Only 2 out of 3 modifications should succeed
|
||||
if executed != 2 {
|
||||
t.Errorf("Expected 2 modifications to be executed successfully, but got %d", executed)
|
||||
}
|
||||
}
|
||||
|
||||
// All valid modifications are executed and the modified string is returned
|
||||
func TestExecuteModificationsAllValid(t *testing.T) {
|
||||
// Setup test data
|
||||
fileData := "Hello world, this is a test"
|
||||
|
||||
modifications := []ReplaceCommand{
|
||||
{From: 0, To: 5, With: "Hi"},
|
||||
{From: 18, To: 20, With: "was"},
|
||||
{From: 21, To: 27, With: "an example"},
|
||||
}
|
||||
|
||||
// Execute the function
|
||||
result, executed := ExecuteModifications(modifications, fileData)
|
||||
|
||||
// Verify results
|
||||
expectedResult := "Hi world, this was an example"
|
||||
if result != expectedResult {
|
||||
t.Errorf("Expected result to be %q, but got %q", expectedResult, result)
|
||||
}
|
||||
|
||||
if executed != 3 {
|
||||
t.Errorf("Expected 3 modifications to be executed, but got %d", executed)
|
||||
}
|
||||
}
|
||||
|
||||
// The count of successfully executed modifications is returned
|
||||
func TestExecuteModificationsReturnsCorrectCount(t *testing.T) {
|
||||
// Setup test data
|
||||
fileData := "Initial text for testing"
|
||||
|
||||
modifications := []ReplaceCommand{
|
||||
{From: 0, To: 7, With: "Final"},
|
||||
{From: 12, To: 16, With: "example"},
|
||||
{From: 17, To: 24, With: "process"},
|
||||
}
|
||||
|
||||
// Execute the function
|
||||
_, executed := ExecuteModifications(modifications, fileData)
|
||||
|
||||
// Verify the count of executed modifications
|
||||
expectedExecuted := 3
|
||||
if executed != expectedExecuted {
|
||||
t.Errorf("Expected %d modifications to be executed, but got %d", expectedExecuted, executed)
|
||||
}
|
||||
}
|
||||
|
||||
// Empty modifications list returns the original string with zero executed count
|
||||
func TestExecuteModificationsWithEmptyList(t *testing.T) {
|
||||
// Setup test data
|
||||
fileData := "This is a test string for replacements"
|
||||
|
||||
modifications := []ReplaceCommand{}
|
||||
|
||||
// Execute the function
|
||||
result, executed := ExecuteModifications(modifications, fileData)
|
||||
|
||||
// Verify results
|
||||
if result != fileData {
|
||||
t.Errorf("Expected result to be %q, but got %q", fileData, result)
|
||||
}
|
||||
|
||||
if executed != 0 {
|
||||
t.Errorf("Expected 0 modifications to be executed, but got %d", executed)
|
||||
}
|
||||
}
|
||||
|
||||
// Modifications with identical 'From' values
|
||||
func TestExecuteModificationsWithIdenticalFromValues(t *testing.T) {
|
||||
// Setup test data
|
||||
fileData := "This is a test string for replacements"
|
||||
|
||||
modifications := []ReplaceCommand{
|
||||
{From: 10, To: 14, With: "sample"},
|
||||
{From: 10, To: 14, With: "example"},
|
||||
{From: 26, To: 38, With: "modifications"},
|
||||
}
|
||||
|
||||
// Execute the function
|
||||
result, executed := ExecuteModifications(modifications, fileData)
|
||||
|
||||
// Verify results
|
||||
// Yes, it's mangled, yes, it's intentional
|
||||
// Every subsequent command works with the modified contents of the previous command
|
||||
// So by the time we get to "example" the indices have already eaten into "sample"... In fact they have eaten into "samp", "le" is left
|
||||
// So we prepend "example" and end up with "examplele"
|
||||
// Whether sample or example goes first here is irrelevant to us
|
||||
// But it just so happens that sample goes first, so we end up with "examplele"
|
||||
expectedResult := "This is a examplele string for modifications"
|
||||
if result != expectedResult {
|
||||
t.Errorf("Expected result to be %q, but got %q", expectedResult, result)
|
||||
}
|
||||
|
||||
if executed != 3 {
|
||||
t.Errorf("Expected 3 modifications to be executed, but got %d", executed)
|
||||
}
|
||||
}
|
||||
|
||||
// Modifications that would affect each other if not sorted properly
|
||||
func TestExecuteModificationsHandlesOverlappingRanges(t *testing.T) {
|
||||
// Setup test data
|
||||
fileData := "The quick brown fox jumps over the lazy dog"
|
||||
|
||||
modifications := []ReplaceCommand{
|
||||
{From: 4, To: 9, With: "slow"},
|
||||
{From: 10, To: 15, With: "red"},
|
||||
{From: 16, To: 19, With: "cat"},
|
||||
}
|
||||
|
||||
// Execute the function
|
||||
result, executed := ExecuteModifications(modifications, fileData)
|
||||
|
||||
// Verify results
|
||||
expectedResult := "The slow red cat jumps over the lazy dog"
|
||||
if result != expectedResult {
|
||||
t.Errorf("Expected result to be %q, but got %q", expectedResult, result)
|
||||
}
|
||||
|
||||
if executed != 3 {
|
||||
t.Errorf("Expected 3 modifications to be executed, but got %d", executed)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user