diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ffaa338..eda104f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,25 +33,25 @@ jobs: - windows-latest # - macOS-latest nim-version: - - 1.2.0 - 1.4.0 + - 1.6.0 - stable needs: before steps: - - uses: actions/checkout@v2 - - name: Cache choosenim - id: cache-choosenim - uses: actions/cache@v1 - with: - path: ~/.choosenim - key: ${{ runner.os }}-choosenim-${{ matrix.nim-version }} - - name: Cache nimble - id: cache-nimble - uses: actions/cache@v1 - with: - path: ~/.nimble - key: ${{ runner.os }}-nimble-${{ matrix.nim-version }} - - uses: jiro4989/setup-nim-action@v1 - with: - nim-version: ${{ matrix.nim-version }} - - run: testament p tests/*.nim + - uses: actions/checkout@v2 + - name: Cache choosenim + id: cache-choosenim + uses: actions/cache@v1 + with: + path: ~/.choosenim + key: ${{ runner.os }}-choosenim-${{ matrix.nim-version }} + - name: Cache nimble + id: cache-nimble + uses: actions/cache@v1 + with: + path: ~/.nimble + key: ${{ runner.os }}-nimble-${{ matrix.nim-version }} + - uses: jiro4989/setup-nim-action@v1 + with: + nim-version: ${{ matrix.nim-version }} + - run: testament p tests/*.nim diff --git a/README.md b/README.md index ce8741b..52ba4df 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,49 @@ interface-implements ![](https://github.com/itsumura-h/nim-interface-implements/workflows/Build%20and%20test%20Nim/badge.svg) -`implements` macro creates `toInterface` proc. It provides polymorphism. -Multiple procedures can be set in `implements` block. +There are two ways to achieve polymorphism in Nim. One is to create `tuple` and another is `dynamic dispatch`. + +**tuple** +```nim +type IRepository* = tuple + exec: proc(msg:string):string +``` + +```nim +type Repository* = object + +proc exec(self:Repository, msg:string):string = + return &"Repository {msg}" + +proc toInterface*(selfRepository):IRepository = + return ( + exec: proc(msg:string):string = self.exec(msg) + ) +``` + +**dynamic dispatch** +```nim +type IRepository* = object of RootObj + +method exec(self:IRepository, msg:string):string {.base.} = raise newException(CatchableError, "error") +``` + +```nim +type Repository* = object of IRepository + +method exec(self:Repository, msg:string):string = + return &"Repository {msg}" +``` + +## install ```sh nimble install interface_implements ``` +## implements +`implements` macro creates `toInterface` proc. + ```nim import interface_implements @@ -22,7 +58,7 @@ implements Repository, IRepository: proc func2(self:Repository, number:int):string = return "Repository2 " & $number ``` -This is converted to bellow +This is converted to bellow. ```nim proc func1(self:Repository, msg:string):string = @@ -38,13 +74,12 @@ proc toInterface*(self:Repository):IRepository = ) ``` -## API +### API ```nim macro implements*(implName, interfaceName, procs:untyped):untyped ``` -## Example - +### Example repository_interface.nim ```nim type IRepository* = tuple @@ -53,8 +88,8 @@ type IRepository* = tuple mock_repository.nim ```nim -import repository_interface import interface_implements +import ./repository_interface type MockRepository = ref object @@ -68,8 +103,8 @@ implements MockRepository, IRepository: repository.nim ```nim -import repository_interface import interface_implements +import ./repository_interface type Repository = ref object @@ -83,7 +118,7 @@ implements Repository, IRepository: usecase.nim ```nim -import repository_interface +import ./repository_interface type Usecase = ref object repository: IRepository @@ -108,4 +143,95 @@ block: assert "Repository exec" == usecase.exec("exec") ``` +## interfaceDef +`interfaceDef` macro creates `method` of fields of Object. +This is only available in Nim 2.0.0 and above. + + +```nim +import interface_implements + +interfaceDef: + type IRepository* = object of RootObj + exec: proc(self:IRepository, msg:string):string +``` +This is converted to bellow. + +```nim +type IRepository* = object of RootObj + +method exec(self:IRepository, msg:string):string {.base.} = raise newException(CatchableError, "Implementation exec of IRepository is not found") +``` + + +### API +```nim +macro interfaceDefs*(body:untyped):untyped +``` + +### Example + +repository_interface.nim +```nim +interfaceDef: + type IRepository* = object of RootObj + exec: proc(msg:string):string +``` + +mock_repository.nim +```nim +import interface_implements + +type MockRepository = object of IRepository + +proc newMockRepository*():MockRepository = + return MockRepository() + +method exec*(self:MockRepository, msg:string):string = + return "MockRepository " & msg +``` + +repository.nim +```nim +import interface_implements + +type Repository = object of IRepository + +proc newRepository*():Repository = + return Repository() + +method exec*(self:Repository, msg:string):string = + return "Repository " & msg +``` + +usecase.nim +```nim +import ./repository_interface + +type Usecase = object + repository: IRepository + +func newUsecase*(repository:IRepository):Usecase = + return Usecase(repository:repository) + +proc exec*(self:Usecase, msg:string):string = + return self.repository.exec(msg) +``` + +presentation layer +```nim +block: + let repository = newMockRepository() + let usecase = newUsecase(repository) + assert "MockRepository mock" == usecase.exec("mock") + +block: + let repository = newRepository() + let usecase = newUsecase(repository) + assert "Repository exec" == usecase.exec("exec") +``` + +--- + +Both pattern achieve structure as bellow. ![](./design.png) diff --git a/examples/field_assign/main.nim b/examples/field_assign/main.nim new file mode 100644 index 0000000..7630945 --- /dev/null +++ b/examples/field_assign/main.nim @@ -0,0 +1,9 @@ +import ./repository +import ./usecase + +proc main() = + let repo = Repository.new() + let usecase = Usecase.new(repo) + usecase.exec() + +main() diff --git a/examples/field_assign/repository.nim b/examples/field_assign/repository.nim new file mode 100644 index 0000000..86f1b16 --- /dev/null +++ b/examples/field_assign/repository.nim @@ -0,0 +1,11 @@ +import ./repository_interface + +type Repository* = object of IRepository + +proc execImpl(self:IRepository, msg:string):string = + return "Repository " & msg + +proc new*(_:type Repository):Repository = + return Repository( + exec:execImpl + ) diff --git a/examples/field_assign/repository_interface.nim b/examples/field_assign/repository_interface.nim new file mode 100644 index 0000000..51e75cc --- /dev/null +++ b/examples/field_assign/repository_interface.nim @@ -0,0 +1,4 @@ +type IRepository* = object of RootObj + exec:proc(self:IRepository, msg:string):string + +proc exec*(self:IRepository, msg:string):string = self.exec(self, msg) diff --git a/examples/field_assign/usecase.nim b/examples/field_assign/usecase.nim new file mode 100644 index 0000000..366b5a6 --- /dev/null +++ b/examples/field_assign/usecase.nim @@ -0,0 +1,12 @@ +import ./repository_interface + +type Usecase* = object + repo:IRepository + +proc new*(_:type Usecase, repo:IRepository):Usecase = + return Usecase( + repo:repo + ) + +proc exec*(self:Usecase) = + echo self.repo.exec("hoge") diff --git a/examples/use_method/main.nim b/examples/use_method/main.nim new file mode 100644 index 0000000..7630945 --- /dev/null +++ b/examples/use_method/main.nim @@ -0,0 +1,9 @@ +import ./repository +import ./usecase + +proc main() = + let repo = Repository.new() + let usecase = Usecase.new(repo) + usecase.exec() + +main() diff --git a/examples/use_method/repository.nim b/examples/use_method/repository.nim new file mode 100644 index 0000000..6e63988 --- /dev/null +++ b/examples/use_method/repository.nim @@ -0,0 +1,9 @@ +import ./repository_interface + +type Repository* = object of IRepository + +proc new*(_:type Repository):Repository = + return Repository() + +method exec*(self:Repository, msg:string):string = + return "Repository " & msg diff --git a/examples/use_method/repository_interface.nim b/examples/use_method/repository_interface.nim new file mode 100644 index 0000000..bd846bb --- /dev/null +++ b/examples/use_method/repository_interface.nim @@ -0,0 +1,3 @@ +type IRepository* = object of RootObj + +method exec*(self:IRepository, msg:string):string {.base.} = raise newException(Exception, "") diff --git a/examples/use_method/usecase.nim b/examples/use_method/usecase.nim new file mode 100644 index 0000000..366b5a6 --- /dev/null +++ b/examples/use_method/usecase.nim @@ -0,0 +1,12 @@ +import ./repository_interface + +type Usecase* = object + repo:IRepository + +proc new*(_:type Usecase, repo:IRepository):Usecase = + return Usecase( + repo:repo + ) + +proc exec*(self:Usecase) = + echo self.repo.exec("hoge") diff --git a/interface_implements.nimble b/interface_implements.nimble index 4f971a7..fceddbb 100644 --- a/interface_implements.nimble +++ b/interface_implements.nimble @@ -1,6 +1,6 @@ # Package -version = "0.2.3" +version = "0.2.4" author = "dumblepy" description = "Creating toInterface macro." license = "MIT" @@ -10,3 +10,13 @@ srcDir = "src" # Dependencies requires "nim >= 1.2.0" + + +import std/strformat +import std/os + +task test, "run testament test": + exec &"testament p 'tests/test_*.nim'" + for kind, path in walkDir(getCurrentDir() / "tests"): + if not path.contains(".") and path.fileExists(): + exec "rm -f " & path diff --git a/src/interface_implements.nim b/src/interface_implements.nim index 2bc474c..42011bf 100644 --- a/src/interface_implements.nim +++ b/src/interface_implements.nim @@ -35,3 +35,46 @@ macro implements*(implName, interfaceName, procs:untyped):untyped = ) """ return (procsStr & '\n' & resultStr).parseStmt + +# ================================================================================ + +# interfaceDefs: +# type IRepository* = object of RootObj +# hoge: proc (self: IRepository, str:string):Future[string] +# type IRepository2* = object of RootObj +# hoge: proc (self: IRepository2, str:string):Future[string] + +when NimMajor >= 2: + macro interfaceDefs*(body:untyped):untyped = + var res = "" + for i, interfaceRows in body: # type IRepository* = object of RootObj | type IRepository2* = object of RootObj + let interfaceName = interfaceRows[0][0].repr.replace("*", "") + let interfaceDef = interfaceRows[0].repr.splitLines()[0] # IRepository* = object of RootObj + var methods:seq[string] + let procRows = interfaceRows[0][2][2] + for procRow in procRows: # hoge: proc (self: IRepository, str:string):Future[string] + let procName = procRow[0].repr.replace("*", "") # hoge + let procDef = procRow[1][0] # (self: IRepository, str:string):Future[string] + var returnType = "" + var args:seq[string] # @["self: IRepository", "str: string"] + var argNames:seq[string] # @["self", "str"] + for i, argsRow in procDef: + if i == 0: + if argsRow.repr.len > 0: returnType = ":" & argsRow.repr # :Future[string] + continue + let argDef = argsRow.repr # self: IRepository | str: string + args.add(argDef) + let argName = argsRow[0].repr # self | str + argNames.add(argName) + + let argsInMethod = args.join(", ") + let methodRow = fmt( + "method [procName]*([argsInMethod])[returnType] {.base.} = raise newException(CatchableError, \"Implementation [procName] of [interfaceName] is not found\")", + '[', + ']', + ) + methods.add(methodRow) + let interfaceStr = if i == 0: &"type {interfaceDef}\n" else: &"\ntype {interfaceDef}\n" + res.add(interfaceStr & methods.join("\n") & "\n" ) + when not defined(release): echo res + return parseStmt(res) diff --git a/tests/interface_defs/mock_repository.nim b/tests/interface_defs/mock_repository.nim new file mode 100644 index 0000000..54e9849 --- /dev/null +++ b/tests/interface_defs/mock_repository.nim @@ -0,0 +1,10 @@ +import std/strformat +import ./repository_interface + +type MockRepository* = object of IRepository + +proc new*(_:type MockRepository):MockRepository = + return MockRepository() + +method exec(self:MockRepository, msg:string):string = + return &"MockRepository {msg}" diff --git a/tests/interface_defs/repository.nim b/tests/interface_defs/repository.nim new file mode 100644 index 0000000..0846867 --- /dev/null +++ b/tests/interface_defs/repository.nim @@ -0,0 +1,10 @@ +import std/strformat +import ./repository_interface + +type Repository* = object of IRepository + +proc new*(_:type Repository):Repository = + return Repository() + +method exec*(self:Repository, msg:string):string = + return &"Repository {msg}" diff --git a/tests/interface_defs/repository_interface.nim b/tests/interface_defs/repository_interface.nim new file mode 100644 index 0000000..e9cbdfb --- /dev/null +++ b/tests/interface_defs/repository_interface.nim @@ -0,0 +1,11 @@ +import ../../src/interface_implements + +interfaceDefs: + type IRepository* = object of RootObj + exec: proc(self:IRepository, msg:string):string + + type IRepository2* = object of RootObj + hoge: proc(self:IRepository) + +# type IRepository* = object of RootObj +# method exec*(self:IRepository, msg:string):string {.base.} = {.warning:"Implementation exec is not found".} diff --git a/tests/interface_defs/usecase.nim b/tests/interface_defs/usecase.nim new file mode 100644 index 0000000..a8bdc4d --- /dev/null +++ b/tests/interface_defs/usecase.nim @@ -0,0 +1,12 @@ +import ./repository_interface + +type Usecase* = object + repo:IRepository + +proc new*(_:type Usecase, repo:IRepository):Usecase = + return Usecase( + repo:repo + ) + +proc exec*(self:Usecase, msg:string):string = + return self.repo.exec(msg) diff --git a/tests/ddd/mock_repository.nim b/tests/interface_implements/mock_repository.nim similarity index 57% rename from tests/ddd/mock_repository.nim rename to tests/interface_implements/mock_repository.nim index e73ab4d..d5e5fde 100644 --- a/tests/ddd/mock_repository.nim +++ b/tests/interface_implements/mock_repository.nim @@ -3,13 +3,10 @@ import repository_interface import ../../src/interface_implements type MockRepository* = ref object - num:int -proc new*(_:type MockRepository, num:int):MockRepository = - return MockRepository( - num:num - ) +proc new*(_:type MockRepository):MockRepository = + return MockRepository() implements MockRepository, IRepository: proc exec(self:MockRepository, msg:string):string = - return &"MockRepository {self.num} {msg}" + return &"MockRepository {msg}" diff --git a/tests/ddd/repository.nim b/tests/interface_implements/repository.nim similarity index 58% rename from tests/ddd/repository.nim rename to tests/interface_implements/repository.nim index 0458ef1..fa2e9ea 100644 --- a/tests/ddd/repository.nim +++ b/tests/interface_implements/repository.nim @@ -3,13 +3,10 @@ import repository_interface import ../../src/interface_implements type Repository* = ref object - key:string -proc new*(_:type Repository, key:string):Repository = - return Repository( - key:key - ) +proc new*(_:type Repository):Repository = + return Repository() implements Repository, IRepository: proc exec(self:Repository, msg:string):string = - return &"Repository {self.key} {msg}" + return &"Repository {msg}" diff --git a/tests/ddd/repository_interface.nim b/tests/interface_implements/repository_interface.nim similarity index 100% rename from tests/ddd/repository_interface.nim rename to tests/interface_implements/repository_interface.nim diff --git a/tests/ddd/usecase.nim b/tests/interface_implements/usecase.nim similarity index 100% rename from tests/ddd/usecase.nim rename to tests/interface_implements/usecase.nim diff --git a/tests/test1.nim b/tests/test1.nim deleted file mode 100644 index 6f9c8f1..0000000 --- a/tests/test1.nim +++ /dev/null @@ -1,14 +0,0 @@ -import ddd/usecase -import ddd/mock_repository -import ddd/repository - - -block: - let repository = MockRepository.new(1).toInterface() - let usecase = Usecase.new(repository) - assert "MockRepository 1 mock" == usecase.exec("mock") - -block: - let repository = Repository.new("a").toInterface() - let usecase = Usecase.new(repository) - assert "Repository a exec" == usecase.exec("exec") diff --git a/tests/test_interface_defs.nim b/tests/test_interface_defs.nim new file mode 100644 index 0000000..48f5adb --- /dev/null +++ b/tests/test_interface_defs.nim @@ -0,0 +1,16 @@ +when NimMajor >= 2: + import ./interface_defs/usecase + import ./interface_defs/mock_repository + import ./interface_defs/repository + + block: + let repository = MockRepository.new() + let usecase = Usecase.new(repository) + echo usecase.exec("mock") + assert "MockRepository mock" == usecase.exec("mock") + + block: + let repository = Repository.new() + let usecase = Usecase.new(repository) + echo usecase.exec("exec") + assert "Repository exec" == usecase.exec("exec") diff --git a/tests/test_interface_implements.nim b/tests/test_interface_implements.nim new file mode 100644 index 0000000..5bc4929 --- /dev/null +++ b/tests/test_interface_implements.nim @@ -0,0 +1,14 @@ +import ./interface_implements/usecase +import ./interface_implements/mock_repository +import ./interface_implements/repository + + +block: + let repository = MockRepository.new().toInterface() + let usecase = Usecase.new(repository) + assert "MockRepository mock" == usecase.exec("mock") + +block: + let repository = Repository.new().toInterface() + let usecase = Usecase.new(repository) + assert "Repository exec" == usecase.exec("exec")