diff --git a/.gitignore b/.gitignore index e44a827..8b4cbda 100644 --- a/.gitignore +++ b/.gitignore @@ -224,3 +224,5 @@ tmp/ # Elixir /test/resources/repos/elixir/test_repo/deps +# Exception: Don't ignore Elixir test repository lib directory (contains source code) +!/test/resources/repos/elixir/test_repo/lib diff --git a/test/resources/repos/elixir/test_repo/lib/examples.ex b/test/resources/repos/elixir/test_repo/lib/examples.ex new file mode 100644 index 0000000..57c2728 --- /dev/null +++ b/test/resources/repos/elixir/test_repo/lib/examples.ex @@ -0,0 +1,211 @@ +defmodule TestRepo.Examples do + @moduledoc """ + Examples module demonstrating usage of models and services. + Similar to Python's examples directory, this shows how different modules work together. + """ + + alias TestRepo.Models.{User, Item} + alias TestRepo.Services.{UserService, ItemService, OrderService} + + defmodule UserManagement do + @doc """ + Creates a complete user workflow example. + """ + def run_user_example do + # Start user service + {:ok, user_service} = UserService.start_link() + + # Create users + {:ok, alice} = UserService.create_user(user_service, "1", "Alice", "alice@example.com", ["admin"]) + {:ok, bob} = UserService.create_user(user_service, "2", "Bob", "bob@example.com", ["user"]) + + # Get users + {:ok, retrieved_alice} = UserService.get_user(user_service, "1") + + # List all users + all_users = UserService.list_users(user_service) + + # Clean up + GenServer.stop(user_service) + + %{ + created_alice: alice, + created_bob: bob, + retrieved_alice: retrieved_alice, + all_users: all_users + } + end + + @doc """ + Demonstrates user role management. + """ + def manage_user_roles do + user = User.new("role_user", "Role User", "role@example.com") + + # Add roles + user_with_admin = User.add_role(user, "admin") + user_with_multiple = User.add_role(user_with_admin, "moderator") + + # Check roles + has_admin = User.has_role?(user_with_multiple, "admin") + has_guest = User.has_role?(user_with_multiple, "guest") + + %{ + original_user: user, + user_with_roles: user_with_multiple, + has_admin: has_admin, + has_guest: has_guest + } + end + end + + defmodule ShoppingExample do + @doc """ + Creates a complete shopping workflow. + """ + def run_shopping_example do + # Create user and items + user = User.new("customer1", "Customer One", "customer@example.com") + item1 = Item.new("widget1", "Super Widget", 19.99, "electronics") + item2 = Item.new("gadget1", "Cool Gadget", 29.99, "electronics") + + # Create order + order = OrderService.create_order("order1", user) + + # Add items to order + order_with_item1 = OrderService.add_item_to_order(order, item1) + order_with_items = OrderService.add_item_to_order(order_with_item1, item2) + + # Process the order + processed_order = OrderService.process_order(order_with_items) + completed_order = OrderService.complete_order(processed_order) + + %{ + user: user, + items: [item1, item2], + final_order: completed_order, + total_cost: completed_order.total + } + end + + @doc """ + Demonstrates item filtering and searching. + """ + def item_filtering_example do + # Start item service + {:ok, item_service} = ItemService.start_link() + + # Create various items + ItemService.create_item(item_service, "laptop", "Gaming Laptop", 1299.99, "electronics") + ItemService.create_item(item_service, "book", "Elixir Guide", 39.99, "books") + ItemService.create_item(item_service, "phone", "Smartphone", 699.99, "electronics") + ItemService.create_item(item_service, "novel", "Great Novel", 19.99, "books") + + # Get all items + all_items = ItemService.list_items(item_service) + + # Filter by category + electronics = ItemService.list_items(item_service, "electronics") + books = ItemService.list_items(item_service, "books") + + # Clean up + Agent.stop(item_service) + + %{ + all_items: all_items, + electronics: electronics, + books: books, + total_items: length(all_items), + electronics_count: length(electronics), + books_count: length(books) + } + end + end + + defmodule IntegrationExample do + @doc """ + Runs a complete e-commerce scenario. + """ + def run_full_scenario do + # Setup services + container = TestRepo.Services.create_service_container() + TestRepo.Services.setup_sample_data(container) + + # Get sample data + {:ok, sample_user} = UserService.get_user(container.user_service, TestRepo.Services.sample_user_id()) + {:ok, sample_item} = ItemService.get_item(container.item_service, TestRepo.Services.sample_item_id()) + + # Create additional items + {:ok, premium_item} = ItemService.create_item( + container.item_service, + "premium", + "Premium Product", + 99.99, + "premium" + ) + + # Create order with multiple items + order = OrderService.create_order("big_order", sample_user, [sample_item]) + order_with_premium = OrderService.add_item_to_order(order, premium_item) + + # Process through order lifecycle + processing_order = OrderService.process_order(order_with_premium) + final_order = OrderService.complete_order(processing_order) + + # Serialize everything for output + serialized_user = TestRepo.Services.serialize_model(sample_user) + serialized_order = TestRepo.Services.serialize_model(final_order) + + # Clean up + GenServer.stop(container.user_service) + Agent.stop(container.item_service) + + %{ + scenario: "full_ecommerce", + user: serialized_user, + order: serialized_order, + total_revenue: final_order.total, + items_sold: length(final_order.items) + } + end + + @doc """ + Demonstrates error handling scenarios. + """ + def error_handling_example do + {:ok, user_service} = UserService.start_link() + + # Try to create duplicate user + {:ok, _user1} = UserService.create_user(user_service, "dup", "User", "user@example.com") + duplicate_result = UserService.create_user(user_service, "dup", "Another User", "another@example.com") + + # Try to get non-existent user + missing_user_result = UserService.get_user(user_service, "nonexistent") + + # Try to delete non-existent user + delete_result = UserService.delete_user(user_service, "nonexistent") + + GenServer.stop(user_service) + + %{ + duplicate_user_error: duplicate_result, + missing_user_error: missing_user_result, + delete_missing_error: delete_result + } + end + end + + @doc """ + Main function to run all examples. + """ + def run_all_examples do + %{ + user_management: UserManagement.run_user_example(), + role_management: UserManagement.manage_user_roles(), + shopping: ShoppingExample.run_shopping_example(), + item_filtering: ShoppingExample.item_filtering_example(), + integration: IntegrationExample.run_full_scenario(), + error_handling: IntegrationExample.error_handling_example() + } + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/lib/ignored_dir/ignored_module.ex b/test/resources/repos/elixir/test_repo/lib/ignored_dir/ignored_module.ex new file mode 100644 index 0000000..a311f1d --- /dev/null +++ b/test/resources/repos/elixir/test_repo/lib/ignored_dir/ignored_module.ex @@ -0,0 +1,23 @@ +defmodule TestRepo.IgnoredDir.IgnoredModule do + @moduledoc """ + This module is in a directory that should be ignored by the language server. + It's used for testing directory filtering functionality. + """ + + alias TestRepo.Models.User + + @doc """ + This function references the User model to test that ignored directories + don't show up in symbol references. + """ + def create_ignored_user do + User.new("ignored", "Ignored User", "ignored@example.com") + end + + @doc """ + Another function that uses models. + """ + def process_ignored_user(user) do + User.add_role(user, "ignored_role") + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/lib/models.ex b/test/resources/repos/elixir/test_repo/lib/models.ex new file mode 100644 index 0000000..4e8a42d --- /dev/null +++ b/test/resources/repos/elixir/test_repo/lib/models.ex @@ -0,0 +1,166 @@ +defmodule TestRepo.Models do + @moduledoc """ + Models module demonstrating various Elixir patterns including structs, protocols, and behaviours. + """ + + defprotocol Serializable do + @doc "Convert model to map representation" + def to_map(model) + end + + defmodule User do + @type t :: %__MODULE__{ + id: String.t(), + name: String.t() | nil, + email: String.t(), + roles: list(String.t()) + } + + defstruct [:id, :name, :email, roles: []] + + @doc """ + Creates a new user. + + ## Examples + + iex> TestRepo.Models.User.new("1", "Alice", "alice@example.com") + %TestRepo.Models.User{id: "1", name: "Alice", email: "alice@example.com", roles: []} + + """ + def new(id, name, email, roles \\ []) do + %__MODULE__{id: id, name: name, email: email, roles: roles} + end + + @doc """ + Checks if user has a specific role. + """ + def has_role?(%__MODULE__{roles: roles}, role) do + role in roles + end + + @doc """ + Adds a role to the user. + """ + def add_role(%__MODULE__{roles: roles} = user, role) do + %{user | roles: [role | roles]} + end + end + + defmodule Item do + @type t :: %__MODULE__{ + id: String.t(), + name: String.t(), + price: float(), + category: String.t() + } + + defstruct [:id, :name, :price, :category] + + @doc """ + Creates a new item. + + ## Examples + + iex> TestRepo.Models.Item.new("1", "Widget", 19.99, "electronics") + %TestRepo.Models.Item{id: "1", name: "Widget", price: 19.99, category: "electronics"} + + """ + def new(id, name, price, category) do + %__MODULE__{id: id, name: name, price: price, category: category} + end + + @doc """ + Formats price for display. + """ + def display_price(%__MODULE__{price: price}) do + "$#{:erlang.float_to_binary(price, decimals: 2)}" + end + + @doc """ + Checks if item is in a specific category. + """ + def in_category?(%__MODULE__{category: category}, target_category) do + category == target_category + end + end + + defmodule Order do + alias TestRepo.Models.{User, Item} + + @type t :: %__MODULE__{ + id: String.t(), + user: User.t(), + items: list(Item.t()), + total: float(), + status: atom() + } + + defstruct [:id, :user, items: [], total: 0.0, status: :pending] + + @doc """ + Creates a new order. + """ + def new(id, user, items \\ []) do + total = calculate_total(items) + %__MODULE__{id: id, user: user, items: items, total: total} + end + + @doc """ + Adds an item to the order. + """ + def add_item(%__MODULE__{items: items} = order, item) do + new_items = [item | items] + %{order | items: new_items, total: calculate_total(new_items)} + end + + @doc """ + Updates order status. + """ + def update_status(%__MODULE__{} = order, status) do + %{order | status: status} + end + + defp calculate_total(items) do + Enum.reduce(items, 0.0, fn item, acc -> acc + item.price end) + end + end + + # Protocol implementations + defimpl Serializable, for: User do + def to_map(%User{id: id, name: name, email: email, roles: roles}) do + %{id: id, name: name, email: email, roles: roles} + end + end + + defimpl Serializable, for: Item do + def to_map(%Item{id: id, name: name, price: price, category: category}) do + %{id: id, name: name, price: price, category: category} + end + end + + defimpl Serializable, for: Order do + def to_map(%Order{id: id, user: user, items: items, total: total, status: status}) do + %{ + id: id, + user: Serializable.to_map(user), + items: Enum.map(items, &Serializable.to_map/1), + total: total, + status: status + } + end + end + + @doc """ + Factory function to create a sample user. + """ + def create_sample_user do + User.new("sample", "Sample User", "sample@example.com", ["user"]) + end + + @doc """ + Factory function to create a sample item. + """ + def create_sample_item do + Item.new("sample", "Sample Item", 9.99, "sample") + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/lib/services.ex b/test/resources/repos/elixir/test_repo/lib/services.ex new file mode 100644 index 0000000..b6768a9 --- /dev/null +++ b/test/resources/repos/elixir/test_repo/lib/services.ex @@ -0,0 +1,257 @@ +defmodule TestRepo.Services do + @moduledoc """ + Services module demonstrating function usage and dependencies. + Similar to Python's services.py, this module uses the models defined in TestRepo.Models. + """ + + alias TestRepo.Models.{User, Item, Order, Serializable} + + defmodule UserService do + use GenServer + + # Client API + + @doc """ + Starts the UserService GenServer. + """ + def start_link(opts \\ []) do + GenServer.start_link(__MODULE__, %{}, opts) + end + + @doc """ + Creates a new user and stores it. + """ + def create_user(pid, id, name, email, roles \\ []) do + GenServer.call(pid, {:create_user, id, name, email, roles}) + end + + @doc """ + Gets a user by ID. + """ + def get_user(pid, id) do + GenServer.call(pid, {:get_user, id}) + end + + @doc """ + Lists all users. + """ + def list_users(pid) do + GenServer.call(pid, :list_users) + end + + @doc """ + Deletes a user by ID. + """ + def delete_user(pid, id) do + GenServer.call(pid, {:delete_user, id}) + end + + # Server callbacks + + @impl true + def init(_) do + {:ok, %{}} + end + + @impl true + def handle_call({:create_user, id, name, email, roles}, _from, users) do + if Map.has_key?(users, id) do + {:reply, {:error, "User with ID #{id} already exists"}, users} + else + user = User.new(id, name, email, roles) + new_users = Map.put(users, id, user) + {:reply, {:ok, user}, new_users} + end + end + + @impl true + def handle_call({:get_user, id}, _from, users) do + case Map.get(users, id) do + nil -> {:reply, {:error, :not_found}, users} + user -> {:reply, {:ok, user}, users} + end + end + + @impl true + def handle_call(:list_users, _from, users) do + user_list = Map.values(users) + {:reply, user_list, users} + end + + @impl true + def handle_call({:delete_user, id}, _from, users) do + if Map.has_key?(users, id) do + new_users = Map.delete(users, id) + {:reply, :ok, new_users} + else + {:reply, {:error, :not_found}, users} + end + end + end + + defmodule ItemService do + use Agent + + @doc """ + Starts the ItemService Agent. + """ + def start_link(opts \\ []) do + Agent.start_link(fn -> %{} end, opts) + end + + @doc """ + Creates a new item and stores it. + """ + def create_item(pid, id, name, price, category) do + Agent.get_and_update(pid, fn items -> + if Map.has_key?(items, id) do + {{:error, "Item with ID #{id} already exists"}, items} + else + item = Item.new(id, name, price, category) + new_items = Map.put(items, id, item) + {{:ok, item}, new_items} + end + end) + end + + @doc """ + Gets an item by ID. + """ + def get_item(pid, id) do + Agent.get(pid, fn items -> + case Map.get(items, id) do + nil -> {:error, :not_found} + item -> {:ok, item} + end + end) + end + + @doc """ + Lists all items, optionally filtered by category. + """ + def list_items(pid, category \\ nil) do + Agent.get(pid, fn items -> + item_list = Map.values(items) + + case category do + nil -> item_list + cat -> Enum.filter(item_list, &Item.in_category?(&1, cat)) + end + end) + end + + @doc """ + Deletes an item by ID. + """ + def delete_item(pid, id) do + Agent.get_and_update(pid, fn items -> + if Map.has_key?(items, id) do + new_items = Map.delete(items, id) + {:ok, new_items} + else + {{:error, :not_found}, items} + end + end) + end + end + + defmodule OrderService do + @doc """ + Creates a new order. + """ + def create_order(id, user, items \\ []) do + Order.new(id, user, items) + end + + @doc """ + Adds an item to an existing order. + """ + def add_item_to_order(order, item) do + Order.add_item(order, item) + end + + @doc """ + Updates the status of an order. + """ + def update_order_status(order, status) do + Order.update_status(order, status) + end + + @doc """ + Processes an order (changes status to :processing). + """ + def process_order(order) do + update_order_status(order, :processing) + end + + @doc """ + Completes an order (changes status to :completed). + """ + def complete_order(order) do + update_order_status(order, :completed) + end + + @doc """ + Cancels an order (changes status to :cancelled). + """ + def cancel_order(order) do + update_order_status(order, :cancelled) + end + end + + @doc """ + Factory function to create a service container. + """ + def create_service_container do + {:ok, user_service} = UserService.start_link() + {:ok, item_service} = ItemService.start_link() + + %{ + user_service: user_service, + item_service: item_service, + order_service: OrderService + } + end + + @doc """ + Helper function to serialize any model that implements the Serializable protocol. + """ + def serialize_model(model) do + Serializable.to_map(model) + end + + # Module-level variables for testing + @sample_user_id "sample_user" + @sample_item_id "sample_item" + + @doc """ + Gets the sample user ID. + """ + def sample_user_id, do: @sample_user_id + + @doc """ + Gets the sample item ID. + """ + def sample_item_id, do: @sample_item_id + + # Create some sample data at module load time + def setup_sample_data(container) do + # Create sample user + UserService.create_user( + container.user_service, + @sample_user_id, + "Sample User", + "sample@example.com", + ["user", "customer"] + ) + + # Create sample item + ItemService.create_item( + container.item_service, + @sample_item_id, + "Sample Widget", + 29.99, + "electronics" + ) + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/lib/test_repo.ex b/test/resources/repos/elixir/test_repo/lib/test_repo.ex new file mode 100644 index 0000000..96a2fd8 --- /dev/null +++ b/test/resources/repos/elixir/test_repo/lib/test_repo.ex @@ -0,0 +1,31 @@ +defmodule TestRepo do + @moduledoc """ + Documentation for `TestRepo`. + """ + + @doc """ + Hello world. + + ## Examples + + iex> TestRepo.hello() + :world + + """ + def hello do + :world + end + + @doc """ + Adds two numbers together. + + ## Examples + + iex> TestRepo.add(2, 3) + 5 + + """ + def add(a, b) do + a + b + end +end \ No newline at end of file diff --git a/test/resources/repos/elixir/test_repo/lib/utils.ex b/test/resources/repos/elixir/test_repo/lib/utils.ex new file mode 100644 index 0000000..458823e --- /dev/null +++ b/test/resources/repos/elixir/test_repo/lib/utils.ex @@ -0,0 +1,48 @@ +defmodule TestRepo.Utils do + @moduledoc """ + Utility functions for TestRepo. + """ + + @doc """ + Converts a string to uppercase. + + ## Examples + + iex> TestRepo.Utils.upcase("hello") + "HELLO" + + """ + def upcase(string) when is_binary(string) do + String.upcase(string) + end + + @doc """ + Calculates the factorial of a number. + + ## Examples + + iex> TestRepo.Utils.factorial(5) + 120 + + """ + def factorial(0), do: 1 + def factorial(n) when n > 0 do + n * factorial(n - 1) + end + + @doc """ + Checks if a number is even. + + ## Examples + + iex> TestRepo.Utils.even?(4) + true + + iex> TestRepo.Utils.even?(3) + false + + """ + def even?(n) when is_integer(n) do + rem(n, 2) == 0 + end +end \ No newline at end of file