├── .gitattributes ├── .gitignore ├── test └── potion │ └── example_test.cljr ├── examples ├── minimal-example │ ├── src │ │ └── minimal_example │ │ │ └── core.cljr │ └── minimal-example.csproj └── dotnet-api │ ├── dotnet-api.csproj │ ├── README.md │ └── src │ └── dotnet_api │ └── main.cljr ├── potion.csproj ├── src ├── NRepl.cs ├── DependencyLoader.cs ├── Tool.cs ├── TestRunner.cs └── DependencyLoaderCsProj.cs ├── README.md └── LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | *.cljr linguist-language=Clojure -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .clj-kondo 2 | .lsp 3 | .portal 4 | bin 5 | obj 6 | publish -------------------------------------------------------------------------------- /test/potion/example_test.cljr: -------------------------------------------------------------------------------- 1 | (ns potion.example-test 2 | (:require [clojure.test :refer [deftest testing is]])) 3 | 4 | (deftest the-tests 5 | (testing "I AM ON A MISSION" 6 | (is (= 1 1))) 7 | (testing "danger is danger" 8 | (is (= "danger" "nope")))) 9 | 10 | -------------------------------------------------------------------------------- /examples/minimal-example/src/minimal_example/core.cljr: -------------------------------------------------------------------------------- 1 | (ns minimal-example.core) 2 | 3 | (defn print-welcome-message [] 4 | (println "Hello Jan, this is the .NET guru at Svea-rikets kommuner.") 5 | (println "Welcoming clojure-clr")) 6 | 7 | (comment 8 | (print-welcome-message) 9 | ) -------------------------------------------------------------------------------- /examples/minimal-example/minimal-example.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | minimal_example 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/dotnet-api/dotnet-api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net7.0 4 | DotNetApi 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/dotnet-api/README.md: -------------------------------------------------------------------------------- 1 | ## dotnet minimal api 2 | Showcases how to use clojure-clr with .NET 7's minimal api. 3 | 4 | ## Usage 5 | connect to a repl and eval the whole file and (start) (stop) in the comment block. 6 | 7 | ### endpoints 8 | localhost:5000/order -- POST -- any json body 9 | localhost:5000/ -- GET -- returns a generated response body based on clojure.spec definitions 10 | 11 | ### I hope that we at one point can get to this state: 12 | 13 | `C#` 14 | ```C# 15 | app.MapGet("/todoitems", async (TodoDb db) => 16 | await db.Todos.ToListAsync()); 17 | ``` 18 | 19 | `Clojure-Clr` 20 | ```Clojure 21 | (.MapGet app "/todoitems", ^:async (fn [^TodoDb db] (await (-> db .-Todos (.ToListAsync))))) 22 | ``` 23 | -------------------------------------------------------------------------------- /potion.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1.0.6 5 | Exe 6 | netcoreapp3.1;net6.0;net7.0 7 | LICENSE 8 | true 9 | Tool.Runner 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/NRepl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using clojure.lang; 5 | 6 | namespace Tool 7 | { 8 | public static class NreplRunner 9 | { 10 | private static readonly Var REQUIRE = RT.var("clojure.core", "require"); 11 | private static readonly Var LOAD = RT.var("clojure.core", "load"); 12 | 13 | public static void Run() 14 | { 15 | 16 | Console.WriteLine("Loading NREPL"); 17 | // FIXME use potion.nrepl to start the server. 18 | // System.Environment.SetEnvironmentVariable("CLOJURE_LOAD_PATH", "src"); 19 | // var ns = "potion.nrepl"; 20 | // REQUIRE.invoke(Symbol.intern(ns)); 21 | // var fn = RT.var(ns, "start-nrepl"); 22 | // fn.invoke(); 23 | 24 | RT.Init(); 25 | 26 | var ns = "clojure.tools.nrepl"; 27 | REQUIRE.invoke(Symbol.intern(ns)); 28 | var fn = RT.var(ns, "start-server!"); 29 | fn.invoke(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Potion 2 | 3 | A [clojure-clr](https://github.com/clojure/clojure-clr) tool for .NET 3.1+ 4 | 5 | ## Installation 6 | `dotnet tool install --global potion` 7 | 8 | `dotnet add package clojure.tools.nrepl --version 0.1.0-alpha1` is needed 9 | because .NET tools does not include dependencies. 10 | 11 | `dotnet restore` 12 | 13 | ## Commands 14 | | Command | Description | 15 | | ------- | ----------- | 16 | | `potion` | Starts a clojure repl with all assemblies loaded from the `.csproj` file | 17 | | `potion repl` | Starts a nrepl server on `localhost:1667` with all assemblies loaded from the `.csproj` file | 18 | | `potion test` | Run tests located in the `test` directory | 19 | 20 | ### Visual Studio Code - Calva Setup 21 | https://calva.io/ 22 | 1. run `potion repl` 23 | 2. Calva: Connect to a Running Repl Server -> Generic -> `localhost:1667` 24 | 25 | 26 | ### Creating a new project 27 | 28 | ```bash 29 | mkdir -p minimal-example && cd minimal-example && dotnet new console --framework net7.0 && dotnet add package clojure.tools.nrepl --version 0.1.0-alpha1 && rm Program.cs 30 | ``` 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2023] [Dangercoder] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/DependencyLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text.Json; 6 | 7 | namespace Tool 8 | { 9 | public static class DependencyLoader 10 | { 11 | static void LoadAllAssembliesInDirectory(string directoryPath) 12 | { 13 | Console.WriteLine($"Loading assemblies in: {directoryPath}"); 14 | 15 | foreach (var filePath in System.IO.Directory.GetFiles(directoryPath)) 16 | { 17 | if (System.IO.Path.GetExtension(filePath) == ".dll") 18 | { 19 | try 20 | { 21 | Assembly.LoadFrom(filePath); 22 | } 23 | catch (Exception) 24 | { 25 | // Ignoring exceptions 26 | } 27 | } 28 | } 29 | } 30 | 31 | public static void Load() 32 | { 33 | var x = 32.GetType().Assembly.Location; 34 | Console.WriteLine(x); 35 | 36 | 37 | var coreAppAssemblyDirectory = Path.GetDirectoryName(x); 38 | var aspNetCoreAppAssemblyDirectory = coreAppAssemblyDirectory.Replace("Microsoft.NETCore.App", "Microsoft.AspNetCore.App"); 39 | 40 | LoadAllAssembliesInDirectory(coreAppAssemblyDirectory); 41 | LoadAllAssembliesInDirectory(aspNetCoreAppAssemblyDirectory); 42 | DependencyLoaderCsProj.LoadDependenciesFromCsproj(); 43 | } 44 | 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/Tool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using clojure.lang; 3 | 4 | namespace Tool 5 | { 6 | public static class Runner 7 | { 8 | private static readonly Symbol CLOJURE_MAIN = Symbol.intern("clojure.main"); 9 | private static readonly Var REQUIRE = RT.var("clojure.core", "require"); 10 | private static readonly Var LEGACY_REPL = RT.var("clojure.main", "legacy-repl"); 11 | private static readonly Var LEGACY_SCRIPT = RT.var("clojure.main", "legacy-script"); 12 | private static readonly Var MAIN = RT.var("clojure.main", "main"); 13 | 14 | static void Main(string[] args) 15 | { 16 | DependencyLoader.Load(); 17 | 18 | if (args.Length > 0 && args[0] == "repl") 19 | { 20 | NreplRunner.Run(); 21 | return; 22 | } 23 | 24 | if (args.Length > 0 && args[0] == "test") 25 | { 26 | TestRunner.Run(); 27 | return; 28 | } 29 | 30 | RT.Init(); 31 | REQUIRE.invoke(CLOJURE_MAIN); 32 | MAIN.applyTo(RT.seq(args)); 33 | } 34 | 35 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "ClojureJVM name match")] 36 | public static void legacy_repl(string[] args) 37 | { 38 | RT.Init(); 39 | REQUIRE.invoke(CLOJURE_MAIN); 40 | LEGACY_REPL.invoke(RT.seq(args)); 41 | 42 | } 43 | 44 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "ClojureJVM name match")] 45 | public static void legacy_script(string[] args) 46 | { 47 | RT.Init(); 48 | REQUIRE.invoke(CLOJURE_MAIN); 49 | LEGACY_SCRIPT.invoke(RT.seq(args)); 50 | } 51 | 52 | 53 | } 54 | } 55 | 56 | 57 | // See https://aka.ms/new-console-template for more information -------------------------------------------------------------------------------- /src/TestRunner.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using clojure.lang; 4 | 5 | namespace Tool 6 | { 7 | public static class TestRunner 8 | { 9 | private static readonly Var REQUIRE = RT.var("clojure.core", "require"); 10 | private static readonly Var LOAD = RT.var("clojure.core", "load"); 11 | 12 | public static void Run(string testRoot = "test", string srcRoot = "src") 13 | { 14 | 15 | 16 | var directories = new List { srcRoot, testRoot }; 17 | 18 | foreach (var directory in directories) 19 | { 20 | if (Directory.Exists(directory)) 21 | { 22 | var cljFiles = Directory.GetFiles(directory, "*.cljr", SearchOption.AllDirectories); 23 | 24 | foreach (var file in cljFiles) 25 | { 26 | var loadPath = Path.GetRelativePath(Directory.GetCurrentDirectory(), file) 27 | .Replace('\\', '/') // Convert backslashes to slashes (for Windows paths) 28 | .Substring(0, file.Length - ".cljr".Length); // Remove the file extension. 29 | 30 | // Convert the file path to a Clojure namespace. 31 | var ns = loadPath 32 | .Replace('/', '.') 33 | .Replace("_", "-"); // Convert slashes to dots. 34 | 35 | // Then require the namespace. 36 | REQUIRE.invoke(Symbol.intern(ns)); 37 | 38 | // ns will start with .test -- remove it so that run-tests works. 39 | if (ns.StartsWith("test.")) 40 | { 41 | ns = ns.Substring("test.".Length); 42 | // TODO system exit. 43 | var testRunner = RT.var("clojure.test", "run-tests"); 44 | testRunner.invoke(Symbol.intern(ns)); 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/dotnet-api/src/dotnet_api/main.cljr: -------------------------------------------------------------------------------- 1 | (ns dotnet-api.main 2 | (:require [clojure.data.json :as json] 3 | [clojure.spec.alpha :as s] 4 | [clojure.spec.gen.alpha :as gen]) 5 | (:import [Microsoft.AspNetCore.Builder EndpointRouteBuilderExtensions] 6 | [Microsoft.AspNetCore.Builder EndpointRouteBuilderExtensions WebApplication] 7 | [Microsoft.AspNetCore.Hosting WebHostBuilderKestrelExtensions] 8 | [Microsoft.AspNetCore.Http HttpRequestRewindExtensions] 9 | [Microsoft.AspNetCore.Http HttpContext HttpResponseWritingExtensions RequestDelegate] 10 | [Microsoft.AspNetCore.Http HttpContext HttpResponseWritingExtensions RequestDelegate] 11 | [Microsoft.AspNetCore.Server.Kestrel.Core KestrelServerOptions] 12 | [System.IO StreamReader] 13 | [System.Threading CancellationTokenSource] 14 | [System.Threading.Tasks Task])) 15 | 16 | (s/def ::home-state #{:dirty :clean :unicorn}) 17 | (s/def ::home-name string?) 18 | (s/def ::home-age pos-int?) 19 | (s/def ::home-id uuid?) 20 | 21 | (s/def ::home-response 22 | (s/keys :req-un [::home-name 23 | ::home-state 24 | ::home-age 25 | ::home-id])) 26 | 27 | (defn ->cancellation-token [] 28 | (-> (new CancellationTokenSource) 29 | (.Token))) 30 | 31 | #_{:clj-kondo/ignore [:unresolved-symbol]} 32 | (defn allow-synchronous-io [builder] 33 | (-> builder 34 | .-WebHost 35 | (WebHostBuilderKestrelExtensions/ConfigureKestrel 36 | (sys-action [KestrelServerOptions] [options] 37 | (set! (.-AllowSynchronousIO options) true)))) 38 | builder) 39 | 40 | (defn home-handler [^HttpContext http-context] 41 | (let [raw-response (.Response http-context) 42 | _ (set! (.-StatusCode raw-response) 200)] 43 | (HttpResponseWritingExtensions/WriteAsync raw-response "Hello, World!"))) 44 | 45 | ;; In C# this is 46 | ;; app.MapGet("/", async (context) => "{}") 47 | (defn home-request-handler [] 48 | (gen-delegate RequestDelegate [^HttpContext http-context] 49 | (Task/Run (sys-action [] [] 50 | (let [request (.Request http-context) 51 | response (.Response http-context) 52 | response-json (json/write-str (gen/generate (s/gen ::home-response)))] 53 | (set! (.-StatusCode response) 200) 54 | (set! (.-ContentType response) "application/json") 55 | (HttpResponseWritingExtensions/WriteAsync response response-json (->cancellation-token))))))) 56 | 57 | (defn order-post-request-handler-async [] 58 | (gen-delegate RequestDelegate [^HttpContext http-context] 59 | (let [request (.Request http-context) 60 | _ (HttpRequestRewindExtensions/EnableBuffering request) 61 | body (-> request .Body) 62 | _ (set! (.Position body) 0)] 63 | (-> (new StreamReader body) 64 | (.ReadToEndAsync) 65 | (.ContinueWith (sys-action [|System.Threading.Tasks.Task`1[System.String]|] [^Task task] 66 | (let [request-json-string (.Result task) ;; this doesn't block since we're in a .ContinueWith block. 67 | response (.Response http-context)] 68 | (set! (.-StatusCode response) 200) 69 | (set! (.-ContentType response) "application/json") 70 | (HttpResponseWritingExtensions/WriteAsync response request-json-string (->cancellation-token)))) 71 | (->cancellation-token)))))) 72 | 73 | 74 | #_{:clj-kondo/ignore [:unresolved-symbol]} 75 | (defn configure-routes [^WebApplication app] 76 | (-> app 77 | (doto (EndpointRouteBuilderExtensions/MapGet "/" (home-request-handler)) 78 | (EndpointRouteBuilderExtensions/MapPost "/order" (order-post-request-handler-async))))) 79 | 80 | (defn ->app [] 81 | (-> (WebApplication/CreateBuilder) 82 | allow-synchronous-io 83 | (.Build) 84 | (configure-routes))) 85 | 86 | (defn stop [app] 87 | (.StopAsync app (->cancellation-token))) 88 | 89 | (defn start [] 90 | (let [app (->app)] 91 | (future (.Run app "http://localhost:5000")) 92 | app)) 93 | 94 | (comment 95 | (def app (start)) 96 | 97 | (stop app) 98 | 99 | :rcf) -------------------------------------------------------------------------------- /src/DependencyLoaderCsProj.cs: -------------------------------------------------------------------------------- 1 | namespace Tool 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Xml.Linq; 7 | using System.IO; 8 | using System.Collections.Generic; 9 | 10 | public static class DependencyLoaderCsProj 11 | { 12 | static List Frameworks; 13 | static HashSet loadedAssemblyPaths = new HashSet(); 14 | 15 | public static void LoadDependenciesFromCsproj() 16 | { 17 | var csprojFilePath = Directory.GetFiles(Directory.GetCurrentDirectory(), "*.csproj").FirstOrDefault(); 18 | 19 | if (csprojFilePath == null) 20 | { 21 | throw new FileNotFoundException("No .csproj file found in the current directory."); 22 | } 23 | 24 | XDocument csprojXml = XDocument.Load(csprojFilePath); 25 | 26 | // .csproj files use namespaces, we need to get it to use in our queries 27 | XNamespace ns = csprojXml.Root.Name.Namespace; 28 | 29 | // Set the target frameworks 30 | Frameworks = new List(); 31 | 32 | // If it's a multi-targeting project 33 | var targetFrameworksProperty = csprojXml.Descendants(ns + "TargetFrameworks").FirstOrDefault(); 34 | if (targetFrameworksProperty != null) 35 | { 36 | var targetFrameworks = targetFrameworksProperty.Value.Split(';').ToList(); 37 | Frameworks.Add(targetFrameworks.Last()); 38 | } 39 | else 40 | { 41 | // If it's a single-targeting project 42 | var targetFrameworkProperty = csprojXml.Descendants(ns + "TargetFramework").FirstOrDefault(); 43 | if (targetFrameworkProperty != null) 44 | { 45 | Frameworks.Add(targetFrameworkProperty.Value); 46 | } 47 | } 48 | 49 | // Select all PackageReference elements 50 | var packageReferences = csprojXml.Descendants(ns + "PackageReference").ToList(); 51 | 52 | foreach (var packageReference in packageReferences) 53 | { 54 | // Get the ID of the package 55 | var id = (string)packageReference.Attribute("Include"); 56 | // Get the version of the package 57 | var version = (string)packageReference.Attribute("Version"); 58 | 59 | // The path to the .nuspec file of the package 60 | var nuspecFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages", id.ToLower(), version, $"{id.ToLower()}.nuspec"); 61 | 62 | // Load dependencies from the .nuspec file 63 | LoadDependenciesFromNuspec(nuspecFilePath); 64 | 65 | // Load the assembly of the package 66 | LoadAssemblyForPackage(id, version); 67 | } 68 | } 69 | 70 | public static void LoadDependenciesFromNuspec(string nuspecFilePath) 71 | { 72 | XDocument nuspecXml = XDocument.Load(nuspecFilePath); 73 | 74 | // .nuspec files use namespaces, we need to get it to use in our queries 75 | XNamespace ns = nuspecXml.Root.GetDefaultNamespace(); 76 | 77 | // Select all dependency elements 78 | var dependencies = nuspecXml.Descendants(ns + "dependencies") 79 | .Elements(ns + "group") 80 | .Elements(ns + "dependency") 81 | .ToList(); 82 | 83 | foreach (var dependency in dependencies) 84 | { 85 | // Get the ID of the dependency 86 | var id = (string)dependency.Attribute("id"); 87 | // Get the version of the dependency 88 | var version = (string)dependency.Attribute("version"); 89 | 90 | // Load the assembly of the dependency 91 | LoadAssemblyForPackage(id, version); 92 | 93 | // Define the path to the .nuspec file of the dependency 94 | var dependencyNuspecFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages", id.ToLower(), version, $"{id.ToLower()}.nuspec"); 95 | 96 | // If the dependency has a .nuspec file, load its dependencies as well 97 | if (File.Exists(dependencyNuspecFilePath)) 98 | { 99 | LoadDependenciesFromNuspec(dependencyNuspecFilePath); 100 | } 101 | } 102 | } 103 | 104 | private static void LoadAssemblyForPackage(string packageId, string version) 105 | { 106 | // Then proceed with loading the assembly 107 | // Define all available frameworks in descending order of compatibility 108 | string[] allFrameworks = { "net8.0", "net7.0", "net6.0", "net5.0", "netcoreapp3.1", "netstandard2.1", "netstandard2.0"}; 109 | 110 | // Determine the starting index in the allFrameworks array based on the target framework 111 | int startIndex = Array.IndexOf(allFrameworks, Frameworks.First()); 112 | 113 | // Loop through the frameworks starting from the target framework 114 | for (int i = startIndex; i < allFrameworks.Length; i++) 115 | { 116 | var assemblyFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages", packageId.ToLower(), version, "lib", allFrameworks[i], $"{packageId}.dll"); 117 | 118 | if (File.Exists(assemblyFilePath)) 119 | { 120 | try 121 | { 122 | // If assembly is already loaded, no need to continue 123 | if (loadedAssemblyPaths.Contains(assemblyFilePath)) 124 | { 125 | return; 126 | } 127 | 128 | var assembly = Assembly.LoadFrom(assemblyFilePath); 129 | loadedAssemblyPaths.Add(assemblyFilePath); 130 | Console.WriteLine(assembly); 131 | return; // Once the assembly is loaded, we don't need to continue the loop 132 | } 133 | catch (Exception e) 134 | { 135 | // This exception is thrown when the file is not a valid assembly, or when the assembly is not compatible 136 | // In this case, we simply ignore the exception and continue to the next framework 137 | } 138 | } 139 | } 140 | } 141 | 142 | 143 | 144 | } 145 | 146 | } --------------------------------------------------------------------------------