diff --git a/.github/workflows/rspec_tests.yaml b/.github/workflows/rspec_tests.yaml index be6ccb80b..6a0bc0e6c 100644 --- a/.github/workflows/rspec_tests.yaml +++ b/.github/workflows/rspec_tests.yaml @@ -31,5 +31,12 @@ jobs: version: 1.0 - name: Run type data migrations to create gameobj-data.xml run: bin/migrate + - name: Upload gameobj-data.xml artifact + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: gameobj-data-ruby-${{ matrix.ruby }} + path: dist/gameobj-data.xml + retention-days: 1 + overwrite: true - name: Run RSpec tests run: bundle exec rspec diff --git a/lib/migration.rb b/lib/migration.rb index f1feee695..4021a5c3c 100644 --- a/lib/migration.rb +++ b/lib/migration.rb @@ -1 +1 @@ -Dir[File.join(__dir__, "migration", "**", "*.rb")].each do |file| require(file) end \ No newline at end of file +Dir[File.join(__dir__, "migration", "**", "*.rb")].each do |file| require(file) end diff --git a/lib/migration/change-set.rb b/lib/migration/change-set.rb index e71c7ef23..3807ba545 100644 --- a/lib/migration/change-set.rb +++ b/lib/migration/change-set.rb @@ -1,37 +1,49 @@ -require "pathname" - module Migration - class PrettyError < Exception + class PrettyError < StandardError def initialize(changeset, msg) super <<~ERROR \n\t#{changeset.file.split("../").last} >> #{changeset.table.log_name} #{msg} ERROR end end + class KeyError < PrettyError; end class RuleError < PrettyError; end + ## ## this is execution instance for ## evaluating a new set of changes ## to be applied to a table ## class ChangeSet - def self.run(table, file, &block) - changeset = ChangeSet.new(table, file) + def self.run(table, file, tables, &block) + changeset = ChangeSet.new(table, file, tables) changeset.instance_eval(&block) changeset end - attr_reader :table, :file, :inserts, :deletes, :creates - def initialize(table, file) + attr_reader :table, :file, :inserts, :deletes, :creates, :tables + + def initialize(table, file, tables) @table = table @file = file + @tables = tables # Add this line @inserts = {} @deletes = {} @creates = [] @table.pending << self end + # Add helper methods + def get_table(table_name) + @tables[Table.normalize_key(table_name)] + end + + def copy_rules_from(source_table_name, key) + source_table = get_table(source_table_name) + source_table.get_rules(key) + end + def will_create?(key) @creates.include?(key) end diff --git a/lib/migration/convert.rb b/lib/migration/convert.rb index 65862bf9e..e7a6a386f 100644 --- a/lib/migration/convert.rb +++ b/lib/migration/convert.rb @@ -1,17 +1,17 @@ module Migration module Convert - # (?:seasoned |grizzled |battle\-scarred |ancient |veteran )? + # (?:seasoned |grizzled |battle\-scarred |ancient |veteran )? def self.maybe_pattern_to_regex(maybes = nil, space: nil, required_match: nil) return %{} if maybes.nil? - #Migration.log(maybes, label: %i[maybes]) + # Migration.log(maybes, label: %i[maybes]) with_whitespace = maybes.map do |maybe| if space.eql?(:left) " " + maybe elsif space.eql?(:right) - maybe + " " + maybe + " " else maybe - end + end end.join("|") if required_match.nil? %{(?:#{with_whitespace})?} @@ -31,4 +31,4 @@ def self.to_safe_xml(str) str.gsub(%[&], %[&]) end end -end \ No newline at end of file +end diff --git a/lib/migration/migration.rb b/lib/migration/migration.rb index 8176b6393..f89d7c343 100644 --- a/lib/migration/migration.rb +++ b/lib/migration/migration.rb @@ -8,8 +8,9 @@ module Migration # creates a raw xml element that does not escape # html entities, so regexp can properly be serialized def self.raw_element(name, parent = nil) - Element.new(name, parent, context = {raw: :all, attribute_quote: :quote}) + Element.new(name, parent, { raw: :all, attribute_quote: :quote }) end + ## ## lookup from cwd ## @@ -25,19 +26,19 @@ def self.log(msg, label: %i[], color: nil) end def self.load_tables(tables) - Hash[tables.map do |table| + Hash[tables.map do |table| table = Table.from_yaml(table) [table.name, table] end] end def self.load_migrations(migrations, tables) - migrations.sort.map do |migration| - Migrator.new(migration, tables) + migrations.sort.map do |migration| + Migrator.new(migration, tables) end end - def self.to_xml(tables) + def self.to_xml(_tables) root = raw_element(%{data}) @tables.values.map(&:to_xml).each do |child| root.add_element(child) @@ -51,9 +52,9 @@ def self.run(**opts) @migrations = Migration.load_migrations(opts.fetch(:migrations), @tables) @migrations.each(&:build).each(&:validate).each(&:apply) asset = File.join(opts.fetch(:dist), "gameobj-data.xml") - Migration.log(%{writing #{asset}}, - label: :write, - color: :pink) + Migration.log(%{writing #{asset}}, + label: :write, + color: :pink) File.open(asset, %{w+}) do |file| file.write Migration.to_xml(@tables).to_s end diff --git a/lib/migration/migrator.rb b/lib/migration/migrator.rb index 6dfedd5a0..43a47f7c2 100644 --- a/lib/migration/migrator.rb +++ b/lib/migration/migrator.rb @@ -1,6 +1,7 @@ module Migration - class TableNotFound < Exception; end - class DuplicateTable < Exception; end + class TableNotFound < StandardError; end + class DuplicateTable < StandardError; end + ## ## loads a migration Ruby instance and executes it ## within a ChangeSet context, storing the result @@ -9,12 +10,14 @@ class DuplicateTable < Exception; end ## class Migrator attr_reader :file, :tables, :basename + def initialize(file, tables) @file = file @tables = tables @basename = File.basename(@file) @changesets = [] end + ## ## read the file into this binding ## @@ -35,33 +38,37 @@ def apply_creations() @changesets.each do |changeset| changeset.creates.each do |key| Migration.log(%[#{changeset.table.log_name} CREATE key "#{key}"], - label: %i[changeset], - color: :light_blue) + label: %i[changeset], + color: :light_blue) changeset.table.create_key(key) end end end def apply_insertions() - @changesets.each do |changeset| + @changesets.each do |changeset| changeset.inserts.each do |key, rules| rules.each do |rule| Migration.log(%[#{changeset.table.log_name} INSERT #{key} "#{rule}"], - label: %i[changeset], - color: :green) + label: %i[changeset], + color: :green) end changeset.table.insert(key, *rules) end end end + def get_table(table_name) + @tables[Table.normalize_key(table_name)] + end + def apply_deletions() - @changesets.each do |changeset| + @changesets.each do |changeset| changeset.deletes.each do |key, rules| rules.each do |rule| Migration.log(%[#{changeset.table.log_name} DELETE #{key} "#{rule}"], - label: %i[changeset], - color: :yellow) + label: %i[changeset], + color: :yellow) end changeset.table.delete(key, *rules) @@ -70,24 +77,24 @@ def apply_deletions() end def assert_table_exists(table_name) - table = @tables[Table.normalize_key(table_name)] or + @tables[Table.normalize_key(table_name)] or fail TableNotFound, Color.red("Table[:#{table_name}] does not exist") end def assert_table_does_not_exist(table_name) - table = @tables[Table.normalize_key(table_name)] and + @tables[Table.normalize_key(table_name)] and fail DuplicateTable, Color.red("Table[:#{table_name}] already exists") end def migrate(*table_names, &migration) table_names.each do |table_name| table = assert_table_exists(table_name) - @changesets << ChangeSet.run(table, @file, &migration) + @changesets << ChangeSet.run(table, @file, @tables, &migration) # Add @tables here end self end - def create_table(table_name, kind: 'type', keys:[]) + def create_table(table_name, kind: 'type', keys: []) assert_table_does_not_exist(table_name) normalized_name = Table.normalize_key(table_name) @tables[normalized_name] = Table.new( diff --git a/lib/migration/table.rb b/lib/migration/table.rb index 39ed432d4..98f9a2a5a 100644 --- a/lib/migration/table.rb +++ b/lib/migration/table.rb @@ -21,27 +21,37 @@ class Table # Empty Ruleset Error # def self.raise_empty_ruleset(table) - fail Exception, <<~ERROR + fail StandardError, <<~ERROR Table[#{table.name}] exported an empty RuleSet and that is not allowed ERROR end + # # Error for bad key validation # def self.raise_bad_rule_kind(table, key) - fail Exception, <<~ERROR + fail StandardError, <<~ERROR Key[#{key}] in Table[#{table.name}] not a valid Key allowed metadata keys: #{METADATA_KEYS} allowed game-obj.xml keys: #{GAMEOBJ_DATA_KEYS} ERROR end + ## ## normalizes a key to what gameobj-data format expects ## def self.normalize_key(key) key.to_s.downcase.to_sym end + + # + # get all rules for a key: + # + def get_rules(key) + @rules[Table.normalize_key(key)] || [] + end + # # validate that a ruleset only contains # whitelisted keys @@ -55,17 +65,17 @@ def self.validate_ruleset(table, ruleset) # # Table Attributes # - attr_reader :name, # the name of the table in gameob-data.xml - # - :rules, # the set of rules that will be applied - # on encoding the table to gameobj-data.xml - # - :pending, # you can mark a table pending - # and it will not compile - # + attr_reader :name, # the name of the table in gameob-data.xml + :rules, # the set of rules that will be applied + # on encoding the table to gameobj-data.xml + # + :pending, # you can mark a table pending + # and it will not compile + # :kind, # sellable | type :basename, # the basename of the table :log_name # name to appear in logs + ## ## reads a .yaml definition into memory ## so that migrations may be layered ontop @@ -84,10 +94,10 @@ def self.from_yaml(file) # output something useful # about how we are loading this data Migration.log(%{decoding #{log_name} from #{file}}, - label: %i[table], - color: :blue) + label: %i[table], + color: :blue) - rules = Hash[YAML.load_file(file).map do |(k,v)| + rules = Hash[YAML.load_file(file).map do |(k, v)| [Table.normalize_key(k), v] end] kind = rules.fetch(:kind, "type") @@ -100,25 +110,29 @@ def self.from_yaml(file) rules: rules ) end + # # check for existance of a key # def has_key?(key) !@rules[Table.normalize_key(key)].nil? end + # # check if a given key has a rule # def has_rule?(key, rule) @rules[Table.normalize_key(key)].include?(rule) end + # - # fetches the current matcher expression + # fetches the current matcher expression # from the table by key # def get(key, default = nil) @rules.fetch(Table.normalize_key(key), default) end + # # inserts a new rule into a table on # the appropriate key @@ -126,6 +140,7 @@ def get(key, default = nil) def insert(key, *rules) @rules[Table.normalize_key(key)] = [*@rules[Table.normalize_key(key)], *rules] end + # # deletes a rule from a table on a key # since we are doing migrations over time @@ -136,14 +151,16 @@ def delete(key, *rules) @rules[Table.normalize_key(key)].delete(rule) end end + # # creates a new key with an empty ruleset # def create_key(key) insert(key) end + # - # dumps the table key/ruleset pairs to + # dumps the table key/ruleset pairs to # a Map(Key, Regexp) that can be used # def to_regex() @@ -162,10 +179,11 @@ def to_regex() @rules.select do |kind| (%i[name prefix prefix_required] & [kind]).empty? end.each do |kind, ruleset| next if ruleset.empty? regex_map[kind] = Validate.regexp(self, kind, - Convert.ruleset_to_regex(ruleset)) + Convert.ruleset_to_regex(ruleset)) end regex_map end + # # compiles the table key/ruleset pairs to # an XML document @@ -180,7 +198,8 @@ def to_xml() element = Migration.raw_element(kind.to_s) xml.add_element(element) element.add_text( - Convert.to_safe_xml(pattern)) + Convert.to_safe_xml(pattern) + ) end ## return the created document return xml diff --git a/lib/migration/validate.rb b/lib/migration/validate.rb index fed0b73bb..46e9ab26a 100644 --- a/lib/migration/validate.rb +++ b/lib/migration/validate.rb @@ -1,5 +1,5 @@ module Migration - class ValidationError < Exception; end + class ValidationError < StandardError; end module Validate def self.regexp(table, key, body) @@ -7,7 +7,7 @@ def self.regexp(table, key, body) Regexp.new(body) rescue => err fail ValidationError, <<~ERROR - + Table[#{table.name}:#{key}] -> Error #{err.message} @@ -16,4 +16,4 @@ def self.regexp(table, key, body) body end end -end \ No newline at end of file +end diff --git a/lib/spec/factories.rb b/lib/spec/factories.rb index 3ffb37218..768f3aefd 100644 --- a/lib/spec/factories.rb +++ b/lib/spec/factories.rb @@ -5,7 +5,7 @@ def self.npc_from_name(npc_name) GameObj.new_npc("fake_id", npc_name.split.last, npc_name) end - def self.item_from_name(item_name, item_noun=item_name.split.last, after_name=nil) + def self.item_from_name(item_name, item_noun = item_name.split.last, after_name = nil) GameObj.new_loot("fake_item_id", item_noun, item_name).tap do |loot| loot.after_name = after_name if after_name end diff --git a/lib/util/color.rb b/lib/util/color.rb index 67f90a1bf..0b1e51bd3 100644 --- a/lib/util/color.rb +++ b/lib/util/color.rb @@ -27,4 +27,4 @@ def self.pink(str) def self.light_blue(str) colorize(str, 36) end -end \ No newline at end of file +end diff --git a/spec/gameobj-data/core_spec.rb b/spec/gameobj-data/core_spec.rb index fcf8448e3..cf95c926e 100644 --- a/spec/gameobj-data/core_spec.rb +++ b/spec/gameobj-data/core_spec.rb @@ -22,6 +22,7 @@ "ascension:quest", "bandit", "boon", + "bottleable", "box", "breakable", "clothing", diff --git a/spec/gameobj-data/gem_spec.rb b/spec/gameobj-data/gem_spec.rb index c6e4aff6a..8089e0b60 100644 --- a/spec/gameobj-data/gem_spec.rb +++ b/spec/gameobj-data/gem_spec.rb @@ -107,14 +107,14 @@ %{yellow zircon}, ].each do |gem| it "recognizes #{gem} as a gem" do - expect(GameObjFactory.item_from_name(gem).type).to eq "gem" + expect(GameObjFactory.item_from_name(gem).type).to include "gem" expect(GameObjFactory.item_from_name(gem).sellable).to eq "gemshop" end end it "recognizes blue lapis lazuli as a gem" do lapis_obj = GameObjFactory.item_from_name("blue lapis lazuli", "lapis") - expect(lapis_obj.type).to eq "gem" + expect(lapis_obj.type).to include "gem" expect(lapis_obj.sellable).to eq "gemshop" end end @@ -198,7 +198,7 @@ %{yellow hyacinth}, ].each do |gem| it "recognizes #{gem} as a gem" do - expect(GameObjFactory.item_from_name(gem).type).to eq "gem" + expect(GameObjFactory.item_from_name(gem).type).to include "gem" expect(GameObjFactory.item_from_name(gem).sellable).to eq "gemshop" end end @@ -235,7 +235,7 @@ %{snowflake zircon}, ].each do |gem| it "recognizes #{gem} as a gem" do - expect(GameObjFactory.item_from_name(gem).type).to eq "gem" + expect(GameObjFactory.item_from_name(gem).type).to include "gem" expect(GameObjFactory.item_from_name(gem).sellable).to eq "gemshop" end end @@ -287,7 +287,7 @@ %{transparent spherine}, ].each do |gem| it "recognizes #{gem} as a gem" do - expect(GameObjFactory.item_from_name(gem).type).to eq "gem" + expect(GameObjFactory.item_from_name(gem).type).to include "gem" expect(GameObjFactory.item_from_name(gem).sellable).to eq "gemshop" end end @@ -386,7 +386,7 @@ %{yellow sunstone}, ].each do |gem| it "recognizes #{gem} as a gem" do - expect(GameObjFactory.item_from_name(gem).type).to eq "gem" + expect(GameObjFactory.item_from_name(gem).type).to include "gem" expect(GameObjFactory.item_from_name(gem).sellable).to eq "gemshop" end end @@ -416,7 +416,7 @@ %{vibrant hummingbird saewehna}, ].each do |gem| it "recognizes #{gem} as a gem" do - expect(GameObjFactory.item_from_name(gem).type).to eq "gem" + expect(GameObjFactory.item_from_name(gem).type).to include "gem" expect(GameObjFactory.item_from_name(gem).sellable).to eq "gemshop" end end @@ -426,7 +426,8 @@ describe "krag dwellers gems" do ["brilliant purple opal"].each do |gem| it "recognizes #{gem} as a gem" do - expect(GameObjFactory.item_from_name(gem).type).to eq "gem" + expect(GameObjFactory.item_from_name(gem).type).to include "gem" + expect(GameObjFactory.item_from_name(gem).sellable).to eq "gemshop" end end end @@ -451,7 +452,7 @@ %{yellow sphene}, ].each do |gem| it "recognizes #{gem} as a gem" do - expect(GameObjFactory.item_from_name(gem).type).to eq "gem" + expect(GameObjFactory.item_from_name(gem).type).to include "gem" expect(GameObjFactory.item_from_name(gem).sellable).to eq "gemshop" end end @@ -471,7 +472,7 @@ %{uncut star-of-tamzyrr diamond}, ].each do |gem| it "recognizes #{gem} as a gem" do - expect(GameObjFactory.item_from_name(gem).type).to eq "gem" + expect(GameObjFactory.item_from_name(gem).type).to include "gem" expect(GameObjFactory.item_from_name(gem).sellable).to eq "gemshop" end end @@ -513,8 +514,8 @@ %{yellow helmet shell}, ].each do |valuable| it "recognizes #{valuable} as a valuable" do - expect(GameObjFactory.item_from_name(valuable).type).to eq "valuable" - expect(GameObjFactory.item_from_name(valuable).sellable).to eq "gemshop" + expect(GameObjFactory.item_from_name(valuable).type).to include "valuable" + expect(GameObjFactory.item_from_name(valuable).sellable).to include "gemshop" end end end @@ -537,7 +538,7 @@ %[twisted iron spiral], ].each do |gem| it "recognizes #{gem} as a gem" do - expect(GameObjFactory.item_from_name(gem).type).to eq "gem" + expect(GameObjFactory.item_from_name(gem).type).to include "gem" expect(GameObjFactory.item_from_name(gem).sellable).to eq "gemshop" end end diff --git a/type_data/migrations/70_bottleable.rb b/type_data/migrations/70_bottleable.rb new file mode 100644 index 000000000..d88da1d40 --- /dev/null +++ b/type_data/migrations/70_bottleable.rb @@ -0,0 +1,87 @@ +create_table "bottleable", keys: [:name, :noun, :exclude] + +migrate "bottleable" do + # insert bottleable "gem" names into :name + copy_rules_from("gem", :name).each do |gem_name| + insert(:name, gem_name) + end + + # insert bottleable "reagent" names into :name + copy_rules_from("reagent", :name).each do |reagent_name| + insert(:name, reagent_name) + end + + # Insert bottleable "valuable" shells to :name + insert(:name, %{amethyst clam shell}) + insert(:name, %{angulate wentletrap shell}) + insert(:name, %{beige clam shell}) + insert(:name, %{black-spined conch shell}) + insert(:name, %{blue-banded coquina shell}) + insert(:name, %{bright noble pectin shell}) + insert(:name, %{blue periwinkle shell}) + insert(:name, %{candystick tellin shell}) + insert(:name, %{checkered chiton shell}) + insert(:name, %{crown conch shell}) + insert(:name, %{crown-of-Charl shell}) + insert(:name, %{dovesnail shell}) + insert(:name, %{egg cowrie shell}) + insert(:name, %{fluted limpet shell}) + insert(:name, %{golden cowrie shell}) + insert(:name, %{large chipped clam shell}) + insert(:name, %{large moonsnail shell}) + insert(:name, %{lavender nassa shell}) + insert(:name, %{leopard cowrie shell}) + insert(:name, %{lynx cowrie shell}) + insert(:name, %{marlin spike shell}) + insert(:name, %{multi-colored snail shell}) + insert(:name, %{opaque spiral shell}) + insert(:name, %{pearl nautilus shell}) + insert(:name, %{piece of iridescent mother-of-pearl}) + insert(:name, %{pink-banded coquina shell}) + insert(:name, %{pink clam shell}) + insert(:name, %{polished batwing chiton shell}) + insert(:name, %{polished black tegula shell}) + insert(:name, %{polished hornsnail shell}) + insert(:name, %{purple-cap cowrie shell}) + insert(:name, %{ruby-lined nassa shell}) + insert(:name, %{sea urchin shell}) + insert(:name, %{silvery clam shell}) + insert(:name, %{snake-head cowrie shell}) + insert(:name, %{snow cowrie shell}) + insert(:name, %{Solhaven Bay scallop shell}) + insert(:name, %{sparkling silvery conch shell}) + insert(:name, %{speckled conch shell}) + insert(:name, %{spiny siren's-comb shell}) + insert(:name, %{spiral turret shell}) + insert(:name, %{striated abalone shell}) + insert(:name, %{sundial shell}) + insert(:name, %{three-lined nassa shell}) + insert(:name, %{tiger cowrie shell}) + insert(:name, %{tiger-striped nautilus shell}) + insert(:name, %{translucent golden spiral shell}) + insert(:name, %{yellow-banded coquina shell}) + insert(:name, %{white clam shell}) + insert(:name, %{white gryphon's wing shell}) + + # insert bottleable "valuable" other stuff to :name + insert(:name, %{polished shark tooth}) + + # insert bottleable "gem" nouns into :noun + copy_rules_from("gem", :noun).each do |gem_name| + insert(:noun, gem_name) + end + + # insert bottleable "reagent" nouns into :noun + copy_rules_from("reagent", :noun).each do |reagent_name| + insert(:noun, reagent_name) + end + + # insert bottleable "gem" exclusions into :exclude + copy_rules_from("gem", :exclude).each do |gem_name| + insert(:exclude, gem_name) + end + + # insert additional "gem" that are not bottleable + insert(:exclude, %{piece of blue ridge coral}) + insert(:exclude, %{piece of cat's-paw coral}) +end